Compare commits
39 Commits
v1.10.0_15
...
v1.13.0_20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
568436f188 | ||
|
|
04b59318f9 | ||
|
|
1a3d05ffc3 | ||
|
|
2f2db74d73 | ||
|
|
ef097d15dd | ||
|
|
caaa474c23 | ||
|
|
63bebd92e0 | ||
|
|
ad36b8b10f | ||
|
|
18c22d2a6c | ||
|
|
73024edba9 | ||
|
|
a360c0a3d7 | ||
|
|
34657f820f | ||
|
|
8840911f22 | ||
|
|
4aa66f4156 | ||
|
|
799a1c99f2 | ||
|
|
c4247bfea3 | ||
|
|
1e3464fe47 | ||
|
|
b7603fd150 | ||
|
|
517a3363d6 | ||
|
|
3511b69fc8 | ||
|
|
e6efc61b3b | ||
|
|
360c1d9a15 | ||
|
|
e3449f9c8f | ||
|
|
a8e723d722 | ||
|
|
d116234523 | ||
|
|
dce2bc7508 | ||
|
|
2bf764f560 | ||
|
|
587b77e70b | ||
|
|
53cd9fd8bf | ||
|
|
a8220172f8 | ||
|
|
397f8c70b4 | ||
|
|
68ff5377b0 | ||
|
|
b359dc3cb6 | ||
|
|
5b036067ed | ||
|
|
b9f38162d5 | ||
|
|
ab6909bfbd | ||
|
|
53c3c916a6 | ||
|
|
6924aa5eb1 | ||
|
|
a3b45d62b6 |
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -16,10 +16,10 @@ Note: Please search to see if an issue already exists for the bug you encountere
|
|||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**Task List**
|
**Task List**
|
||||||
[ ] I have read thoroughly the README setup and installation instructions.
|
- [ ] I have read thoroughly the README setup and installation instructions.
|
||||||
[ ] If my setup is different, I have included my docker-compose file.
|
- [ ] If my setup is different, I have included my docker-compose file.
|
||||||
[ ] I have included my redacted `.env` file.
|
- [ ] I have included my redacted `.env` file.
|
||||||
[ ] I have included information on my machine, and environment.
|
- [ ] I have included information on my machine, and environment.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|||||||
27
.github/workflows/build_push_docker_latest.yml
vendored
@@ -4,17 +4,16 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_push_server_latest:
|
# This image include both the server and microservices - the two containers can be slitted into separated
|
||||||
|
# service with its coressponding entry file.
|
||||||
|
build_and_push_server_monorepo_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
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -27,23 +26,22 @@ jobs:
|
|||||||
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 Mono Repo
|
||||||
uses: docker/build-push-action@v3.0.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-server:latest
|
altran1502/immich-server:latest
|
||||||
|
|
||||||
build_and_push_microservice_latest:
|
build_and_push_machine_learning_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
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -56,15 +54,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Microservices
|
- name: Build and Push Machine Learning
|
||||||
uses: docker/build-push-action@v3.0.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: ./microservices
|
context: ./machine-learning
|
||||||
file: ./microservices/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64
|
platforms: linux/arm/v7,linux/amd64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-microservices:latest
|
altran1502/immich-machine-learning:latest
|
||||||
|
|
||||||
build_and_push_web_latest:
|
build_and_push_web_latest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -72,7 +70,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
# ref: "main" # branch
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.0.0
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
@@ -91,6 +88,6 @@ jobs:
|
|||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
target: prod
|
target: prod
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-web:latest
|
altran1502/immich-web:latest
|
||||||
|
|||||||
95
.github/workflows/build_push_docker_staging.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
name: Build and Push Docker Image - Staging
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# This image include both the server and microservices - the two containers can be slitted into separated
|
||||||
|
# service with its coressponding entry file.
|
||||||
|
build_and_push_server_monorepo_staging:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push Immich Mono Repo
|
||||||
|
uses: docker/build-push-action@v3.0.0
|
||||||
|
with:
|
||||||
|
context: ./server
|
||||||
|
file: ./server/Dockerfile
|
||||||
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name == 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-server:staging
|
||||||
|
|
||||||
|
build_and_push_machine_learning_staging:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and Push Machine Learning
|
||||||
|
uses: docker/build-push-action@v3.0.0
|
||||||
|
with:
|
||||||
|
context: ./machine-learning
|
||||||
|
file: ./machine-learning/Dockerfile
|
||||||
|
platforms: linux/arm/v7,linux/amd64
|
||||||
|
push: ${{ github.event_name == 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-machine-learning:staging
|
||||||
|
|
||||||
|
build_and_push_web_staging:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and Push 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:staging
|
||||||
20
.github/workflows/build_push_server_release.yml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_push_server_release:
|
build_and_push_server_monorepo_release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -44,43 +44,37 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-server:${{ steps.previoustag.outputs.tag }}
|
altran1502/immich-server:${{ steps.previoustag.outputs.tag }}
|
||||||
|
|
||||||
build_and_push_microservice_release:
|
build_and_push_machine_learning_release:
|
||||||
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"
|
|
||||||
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@v2.0.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@v2.0.0
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and Push Machine Learning
|
||||||
- name: Build and push immich-microservices release
|
|
||||||
uses: docker/build-push-action@v3.0.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: ./microservices
|
context: ./machine-learning
|
||||||
file: ./microservices/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64
|
platforms: linux/arm/v7,linux/amd64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-microservices:${{ steps.previoustag.outputs.tag }}
|
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
|
||||||
|
|
||||||
build_and_push_web_release:
|
build_and_push_web_release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
17
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Test
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push: { branches: master }
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-server-e2e:
|
||||||
|
name: Run test suite
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Run Immich Server 2E2 Test
|
||||||
|
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich_server_test
|
||||||
6
Makefile
@@ -5,16 +5,16 @@ 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:
|
stage:
|
||||||
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
test-e2e:
|
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
|
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich_server_test --remove-orphans
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich_server=3 --scale immich_microservices=3 --remove-orphans
|
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=5 --scale immich-microservices=3 --remove-orphans
|
||||||
63
README.md
@@ -8,22 +8,24 @@
|
|||||||
<img src="https://img.shields.io/teamcity/http/immichci.little-home.net/s/Immich_BuildAndPublishIOSToTestFlight.svg?style=for-the-badge&label=iOS&logo=teamcity&logoColor=000000&labelColor=ececec" alt="iOS Build"/>
|
<img src="https://img.shields.io/teamcity/http/immichci.little-home.net/s/Immich_BuildAndPublishIOSToTestFlight.svg?style=for-the-badge&label=iOS&logo=teamcity&logoColor=000000&labelColor=ececec" alt="iOS Build"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://actions-badge.atrox.dev/alextran1502/immich/goto?ref=main">
|
<a href="https://actions-badge.atrox.dev/alextran1502/immich/goto?ref=main">
|
||||||
<img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Falextran1502%2Fimmich%2Fbadge%3Fref%3Dmain&style=for-the-badge&label=Server Docker&logo=docker&labelColor=ececec" />
|
<img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Falextran1502%2Fimmich%2Fbadge%3Fref%3Dmain&style=for-the-badge&label=Github Action&logo=github&labelColor=ececec&logoColor=000000" />
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/rxnyVTXGbM">
|
||||||
|
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Immich%20Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Immich Discord"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="design/immich-logo.svg" width="200" title="Immich Logo">
|
<img src="design/feature-panel.png" title="Immich Logo">
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Immich
|
# Immich
|
||||||
|
|
||||||
Self-hosted photo and video backup solution directly from your mobile phone.
|
**High performance self-hosted photo and video backup solution.**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ Loading ~4000 images/videos
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
### Mobile client
|
### Mobile
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/login-screen.png" width="150" title="Login With Custom URL">
|
<img src="design/login-screen.png" width="150" title="Login With Custom URL">
|
||||||
<img src="design/backup-screen.png" width="150" title="Backup Setting Info">
|
<img src="design/backup-screen.png" width="150" title="Backup Setting Info">
|
||||||
@@ -42,9 +44,10 @@ Loading ~4000 images/videos
|
|||||||
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
### Web client
|
### Web
|
||||||
<p align="center">
|
<p align="left">
|
||||||
<img src="design/dashboard_photos.jpeg" width="100%" title="Home Dashboard">
|
<img src="design/web-home.jpeg" width="49%" title="Home Dashboard">
|
||||||
|
<img src="design/web-detail.jpeg" width="49%" title="Detail">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Note
|
# Note
|
||||||
@@ -55,24 +58,20 @@ This project is under heavy development, there will be continuous functions, fea
|
|||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- Upload and view assets (videos/images).
|
| | Mobile | Web |
|
||||||
- Auto Backup.
|
| - | - | - |
|
||||||
- Download asset to local device.
|
| Upload and view videos and photos | Yes | Yes
|
||||||
- Multi-user supported.
|
| Auto backup when app is opened | Yes | N/A
|
||||||
- Quick navigation with drag scroll bar.
|
| Selective album(s) for backup | Yes | N/A
|
||||||
- Support HEIC/HEIF Backup.
|
| Download photos and videos to local device | Yes | Yes
|
||||||
- Extract and display EXIF info.
|
| Multi-user support | Yes | Yes
|
||||||
- Real-time render from multi-device upload event.
|
| Shared Albums | Yes | No
|
||||||
- Image Tagging/Classification based on ImageNet dataset
|
| Quick navigation with draggable scrollbar | Yes | Yes
|
||||||
- Object detection based on COCO SSD.
|
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
|
||||||
- Search assets based on tags and exif data (lens, make, model, orientation)
|
| Metadata view (EXIF, map) | Yes | Yes
|
||||||
- [Optional] Reverse geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
| Search by metadata, objects and image tags | Yes | No
|
||||||
- Show asset's location information on map (OpenStreetMap).
|
| Administrative functions (user management) | No | Yes
|
||||||
- Show curated places on the search page
|
|
||||||
- Show curated objects on the search page
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
# System Requirement
|
# System Requirement
|
||||||
|
|
||||||
@@ -95,7 +94,7 @@ You can use docker compose for development and testing out the application, ther
|
|||||||
3. **PostgreSQL** - Main database of the application
|
3. **PostgreSQL** - Main database of the application
|
||||||
4. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
|
4. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
|
||||||
5. **Nginx** - Load balancing and optimized file uploading.
|
5. **Nginx** - Load balancing and optimized file uploading.
|
||||||
6. **TensorFlow** - Object Detection and Image Classification.
|
6. **TensorFlow** - Object Detection (COCO SSD) and Image Classification (ImageNet).
|
||||||
|
|
||||||
## Step 1: Populate .env file
|
## Step 1: Populate .env file
|
||||||
|
|
||||||
@@ -144,8 +143,8 @@ MAPBOX_KEY=
|
|||||||
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
||||||
# know where can it make the request to.
|
# know where can it make the request to.
|
||||||
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
# 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
|
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
|
||||||
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283
|
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283/api
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 2: Start the server
|
## Step 2: Start the server
|
||||||
@@ -168,11 +167,11 @@ To *update* docker-compose with newest image (if you have started the docker-com
|
|||||||
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
The server will be running at `http://your-ip:2283` through `Nginx`
|
The server will be running at `http://your-ip:2283/api` through `Nginx`
|
||||||
|
|
||||||
## Step 3: Register User
|
## Step 3: Register User
|
||||||
|
|
||||||
Access the web interface at `http://your-ip:2285` to register an admin account.
|
Access the web interface at `http://your-ip:2283` to register an admin account.
|
||||||
|
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
||||||
@@ -257,5 +256,5 @@ You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
|
|||||||
`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host`
|
`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host`
|
||||||
|
|
||||||
Otherwise you can:
|
Otherwise you can:
|
||||||
- edit `docker-compose.yml` file and comment the whole `immich_microservices` service **which will disable machine learning features like object detection and image classification**
|
- edit `docker-compose.yml` file and comment the whole `immich-machine-learning` service **which will disable machine learning features like object detection and image classification**
|
||||||
- switch to a different VM/desktop with different architecture.
|
- switch to a different VM/desktop with different architecture.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 154 KiB |
BIN
design/feature-panel.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
design/web-admin.jpeg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
design/web-detail.jpeg
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
design/web-home.jpeg
Normal file
|
After Width: | Height: | Size: 206 KiB |
@@ -57,7 +57,7 @@ MAPBOX_KEY=
|
|||||||
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
||||||
# know where can it make the request to.
|
# know where can it make the request to.
|
||||||
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
# 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
|
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
|
||||||
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
|
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
|
||||||
|
|
||||||
VITE_SERVER_ENDPOINT=
|
VITE_SERVER_ENDPOINT=
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
DB_HOSTNAME=immich_postgres_test
|
|
||||||
# Database
|
# Database
|
||||||
|
DB_HOSTNAME=immich_postgres_test
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
DB_DATABASE_NAME=e2e_test
|
DB_DATABASE_NAME=e2e_test
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOSTNAME=immich_redis_test
|
||||||
|
|
||||||
# Upload File Config
|
# Upload File Config
|
||||||
UPLOAD_LOCATION=./upload
|
UPLOAD_LOCATION=./upload
|
||||||
|
|
||||||
@@ -13,4 +16,7 @@ JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
|||||||
# MAPBOX
|
# MAPBOX
|
||||||
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
ENABLE_MAPBOX=false
|
ENABLE_MAPBOX=false
|
||||||
|
|
||||||
|
# WEB
|
||||||
MAPBOX_KEY=
|
MAPBOX_KEY=
|
||||||
|
VITE_SERVER_ENDPOINT=http://localhost:2283/api
|
||||||
@@ -2,11 +2,11 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
image: immich-server-dev:1.9.0
|
image: immich-server-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
command: npm run start:dev
|
command: npm run start:dev immich
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -20,19 +20,17 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
immich-microservices:
|
immich-machine-learning:
|
||||||
image: immich-microservices-dev:1.9.0
|
image: immich-machine-learning-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../microservices
|
context: ../machine-learning
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
command: npm run start:dev
|
command: npm run start:dev
|
||||||
expose:
|
expose:
|
||||||
- "3001"
|
- "3001"
|
||||||
volumes:
|
volumes:
|
||||||
- ../microservices:/usr/src/app
|
- ../machine-learning:/usr/src/app
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
env_file:
|
env_file:
|
||||||
@@ -41,8 +39,24 @@ services:
|
|||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
immich-microservices:
|
||||||
|
image: immich-microservices:latest
|
||||||
|
build:
|
||||||
|
context: ../server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
command: npm run start:dev microservices
|
||||||
|
volumes:
|
||||||
|
- ../server:/usr/src/app
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
- immich-server
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
image: immich-web-dev:1.9.0
|
image: immich-web-dev:1.9.0
|
||||||
@@ -54,20 +68,16 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 3002:3002
|
- 3002:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ../web:/usr/src/app
|
- ../web:/usr/src/app
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -83,8 +93,6 @@ services:
|
|||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
container_name: proxy_nginx
|
container_name: proxy_nginx
|
||||||
@@ -96,12 +104,8 @@ services:
|
|||||||
- 2284:443
|
- 2284:443
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-server
|
- immich-server
|
||||||
|
|
||||||
networks:
|
|
||||||
immich-network:
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
immich-server:
|
|
||||||
image: immich-server-dev:1.9.0
|
|
||||||
build:
|
|
||||||
context: ../server
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
command: npm run start:dev
|
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
volumes:
|
|
||||||
- ../server:/usr/src/app
|
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
- /usr/src/app/node_modules
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- database
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
immich-microservices:
|
|
||||||
image: immich-microservices-dev:1.9.0
|
|
||||||
build:
|
|
||||||
context: ../microservices
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
command: npm run start:dev
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: 1
|
|
||||||
capabilities: [ gpu ]
|
|
||||||
expose:
|
|
||||||
- "3001"
|
|
||||||
volumes:
|
|
||||||
- ../microservices:/usr/src/app
|
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
- /usr/src/app/node_modules
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
- database
|
|
||||||
- immich_server
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
redis:
|
|
||||||
container_name: immich_redis
|
|
||||||
image: redis:6.2
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
networks:
|
|
||||||
immich-network:
|
|
||||||
volumes:
|
|
||||||
pgdata:
|
|
||||||
@@ -2,13 +2,8 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
image: immich-server-staging:latest
|
image: altran1502/immich-server:staging
|
||||||
build:
|
entrypoint: ["/bin/sh", "./start-server.sh"]
|
||||||
context: ../server
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
@@ -18,18 +13,25 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
image: immich-microservices-staging:latest
|
image: altran1502/immich-server:staging
|
||||||
build:
|
entrypoint: ["/bin/sh", "./start-microservices.sh"]
|
||||||
context: ../microservices
|
volumes:
|
||||||
dockerfile: Dockerfile
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- database
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
immich-machine-learning:
|
||||||
|
image: altran1502/immich-machine-learning:staging
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
|
||||||
- "3001"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
@@ -38,34 +40,20 @@ services:
|
|||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
image: immich-web-staging:latest
|
image: altran1502/immich-web:staging
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
build:
|
|
||||||
context: ../web
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
target: prod
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
|
||||||
- 2285:3000
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
@@ -78,10 +66,7 @@ services:
|
|||||||
PG_DATA: /var/lib/postgresql/data
|
PG_DATA: /var/lib/postgresql/data
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
restart: always
|
||||||
- 5432:5432
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
container_name: proxy_nginx
|
container_name: proxy_nginx
|
||||||
@@ -93,13 +78,9 @@ services:
|
|||||||
- 2284:443
|
- 2284:443
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-server
|
- immich-server
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
networks:
|
|
||||||
immich-network:
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server_test:
|
immich_server_test:
|
||||||
image: immich-server-dev:1.9.0
|
image: immich-server-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -19,15 +19,10 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich_network_test
|
|
||||||
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis_test
|
container_name: immich_redis_test
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
|
||||||
- immich_network_test
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres_test
|
container_name: immich_postgres_test
|
||||||
@@ -40,13 +35,6 @@ services:
|
|||||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
PG_DATA: /var/lib/postgresql/data
|
PG_DATA: /var/lib/postgresql/data
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata-test:/var/lib/postgresql/data
|
- /var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
|
||||||
- immich_network_test
|
|
||||||
|
|
||||||
networks:
|
|
||||||
immich_network_test:
|
|
||||||
volumes:
|
|
||||||
pgdata-test:
|
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ version: "3.8"
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
image: altran1502/immich-server:latest
|
image: altran1502/immich-server:latest
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./start-server.sh"]
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
@@ -15,15 +13,25 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
image: altran1502/immich-microservices:latest
|
image: altran1502/immich-server:latest
|
||||||
|
entrypoint: ["/bin/sh", "./start-microservices.sh"]
|
||||||
|
volumes:
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- database
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
immich-machine-learning:
|
||||||
|
image: altran1502/immich-machine-learning:latest
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
|
||||||
- "3001"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
@@ -32,8 +40,6 @@ services:
|
|||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
@@ -41,21 +47,13 @@ services:
|
|||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
|
||||||
- 2285:3000
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
@@ -68,10 +66,6 @@ services:
|
|||||||
PG_DATA: /var/lib/postgresql/data
|
PG_DATA: /var/lib/postgresql/data
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
@@ -84,13 +78,9 @@ services:
|
|||||||
- 2284:443
|
- 2284:443
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-server
|
- immich-server
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
networks:
|
|
||||||
immich-network:
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
@@ -19,6 +19,33 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
gzip_static on;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_comp_level 2;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_buffer_size 16k;
|
||||||
|
proxy_busy_buffers_size 24k;
|
||||||
|
proxy_buffers 64 4k;
|
||||||
|
proxy_force_ranges on;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
|
rewrite /api/(.*) /$1 break;
|
||||||
|
|
||||||
|
proxy_pass http://immich-server:3000;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|
||||||
# Compression
|
# Compression
|
||||||
@@ -41,6 +68,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-web:3000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,3 +33,5 @@ lerna-debug.log*
|
|||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
upload/
|
||||||
@@ -7,7 +7,7 @@ export class ImageClassifierController {
|
|||||||
private readonly imageClassifierService: ImageClassifierService,
|
private readonly imageClassifierService: ImageClassifierService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@Post('/tagImage')
|
@Post('/tag-image')
|
||||||
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
|
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
return await this.imageClassifierService.tagImage(thumbnailPath);
|
return await this.imageClassifierService.tagImage(thumbnailPath);
|
||||||
}
|
}
|
||||||
@@ -8,14 +8,14 @@ async function bootstrap() {
|
|||||||
await app.listen(3001, () => {
|
await app.listen(3001, () => {
|
||||||
if (process.env.NODE_ENV == 'development') {
|
if (process.env.NODE_ENV == 'development') {
|
||||||
Logger.log(
|
Logger.log(
|
||||||
'Running Immich Microservices in DEVELOPMENT environment',
|
'Running Immich Machine Learning in DEVELOPMENT environment',
|
||||||
'IMMICH MICROSERVICES',
|
'IMMICH MICROSERVICES',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV == 'production') {
|
if (process.env.NODE_ENV == 'production') {
|
||||||
Logger.log(
|
Logger.log(
|
||||||
'Running Immich Microservices in PRODUCTION environment',
|
'Running Immich Machine Learning in PRODUCTION environment',
|
||||||
'IMMICH MICROSERVICES',
|
'IMMICH MICROSERVICES',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ export class ObjectDetectionController {
|
|||||||
private readonly objectDetectionService: ObjectDetectionService,
|
private readonly objectDetectionService: ObjectDetectionService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@Post('/detectObject')
|
@Post('/detect-object')
|
||||||
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
|
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
return await this.objectDetectionService.detectObject(thumbnailPath);
|
return await this.objectDetectionService.detectObject(thumbnailPath);
|
||||||
}
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
devenv/
|
|
||||||
3
machine_learning/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
devenv/
|
|
||||||
app/upload
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
## GPU Build
|
|
||||||
# FROM tensorflow/tensorflow:latest-gpu as gpu
|
|
||||||
|
|
||||||
# WORKDIR /code
|
|
||||||
|
|
||||||
# COPY ./requirements.txt /code/requirements.txt
|
|
||||||
|
|
||||||
# RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
|
||||||
|
|
||||||
# COPY ./app /code/app
|
|
||||||
|
|
||||||
|
|
||||||
## CPU BUILD
|
|
||||||
FROM python:3.8 as cpu
|
|
||||||
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get install ffmpeg libsm6 libxext6 -y
|
|
||||||
|
|
||||||
WORKDIR /code
|
|
||||||
|
|
||||||
COPY ./requirements.txt /code/requirements.txt
|
|
||||||
|
|
||||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
|
||||||
|
|
||||||
COPY ./app /code/app
|
|
||||||
|
Before Width: | Height: | Size: 193 KiB |
@@ -1,37 +0,0 @@
|
|||||||
from tensorflow.keras.applications import InceptionV3
|
|
||||||
from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions
|
|
||||||
from tensorflow.keras.preprocessing import image
|
|
||||||
import numpy as np
|
|
||||||
from PIL import Image
|
|
||||||
import cv2
|
|
||||||
IMG_SIZE = 299
|
|
||||||
PREDICTION_MODEL = InceptionV3(weights='imagenet')
|
|
||||||
|
|
||||||
|
|
||||||
def classify_image(image_path: str):
|
|
||||||
img_path = f'./app/{image_path}'
|
|
||||||
# img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE))
|
|
||||||
|
|
||||||
target_image = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)
|
|
||||||
resized_target_image = cv2.resize(target_image, (IMG_SIZE, IMG_SIZE))
|
|
||||||
|
|
||||||
x = image.img_to_array(resized_target_image)
|
|
||||||
x = np.expand_dims(x, axis=0)
|
|
||||||
x = preprocess_input(x)
|
|
||||||
|
|
||||||
preds = PREDICTION_MODEL.predict(x)
|
|
||||||
result = decode_predictions(preds, top=3)[0]
|
|
||||||
payload = []
|
|
||||||
for _, value, _ in result:
|
|
||||||
payload.append(value)
|
|
||||||
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def warm_up():
|
|
||||||
img_path = f'./app/test.png'
|
|
||||||
img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE))
|
|
||||||
x = image.img_to_array(img)
|
|
||||||
x = np.expand_dims(x, axis=0)
|
|
||||||
x = preprocess_input(x)
|
|
||||||
PREDICTION_MODEL.predict(x)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
from .object_detection import object_detection
|
|
||||||
from .image_classifier import image_classifier
|
|
||||||
|
|
||||||
from tf2_yolov4.anchors import YOLOV4_ANCHORS
|
|
||||||
from tf2_yolov4.model import YOLOv4
|
|
||||||
|
|
||||||
|
|
||||||
HEIGHT, WIDTH = (640, 960)
|
|
||||||
|
|
||||||
# Warm up model
|
|
||||||
image_classifier.warm_up()
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
|
|
||||||
class TagImagePayload(BaseModel):
|
|
||||||
thumbnail_path: str
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/tagImage")
|
|
||||||
async def post_root(payload: TagImagePayload):
|
|
||||||
image_path = payload.thumbnail_path
|
|
||||||
|
|
||||||
if image_path[0] == '.':
|
|
||||||
image_path = image_path[2:]
|
|
||||||
|
|
||||||
return image_classifier.classify_image(image_path=image_path)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def test():
|
|
||||||
|
|
||||||
object_detection.run_detection()
|
|
||||||
# image = tf.io.read_file("./app/cars.jpg")
|
|
||||||
# image = tf.image.decode_image(image)
|
|
||||||
# image = tf.image.resize(image, (HEIGHT, WIDTH))
|
|
||||||
# images = tf.expand_dims(image, axis=0) / 255.0
|
|
||||||
|
|
||||||
# model = YOLOv4(
|
|
||||||
# (HEIGHT, WIDTH, 3),
|
|
||||||
# 80,
|
|
||||||
# YOLOV4_ANCHORS,
|
|
||||||
# "darknet",
|
|
||||||
# )
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
def run_detection():
|
|
||||||
print("run detection")
|
|
||||||
|
Before Width: | Height: | Size: 345 KiB |
@@ -1,8 +0,0 @@
|
|||||||
opencv-python==4.5.5.64
|
|
||||||
fastapi>=0.68.0,<0.69.0
|
|
||||||
pydantic>=1.8.0,<2.0.0
|
|
||||||
uvicorn>=0.15.0,<0.16.0
|
|
||||||
tensorflow==2.8.0
|
|
||||||
numpy==1.22.2
|
|
||||||
pillow==9.0.1
|
|
||||||
tf2_yolov4==0.1.0
|
|
||||||
@@ -1,10 +1,45 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
#
|
#
|
||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b
|
revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
channel: stable
|
channel: stable
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: android
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: ios
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: linux
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: macos
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: web
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: windows
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ linter:
|
|||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
use_build_context_synchronously: false
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|||||||
@@ -81,5 +81,4 @@ flutter {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,11 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// Generated 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;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.CallSuper;
|
|
||||||
import androidx.multidex.MultiDex;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension of {@link android.app.Application}, adding multidex support.
|
|
||||||
*/
|
|
||||||
public class FlutterMultiDexApplication extends Application {
|
|
||||||
@Override
|
|
||||||
@CallSuper
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
MultiDex.install(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Update to Material Design 3
|
||||||
|
* Fixed back button navigation - no longer return back to the home page
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Added announcement pop-up when a new released is pushed out in Github.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Fixed crash issue when upload large file on slow network
|
||||||
|
* Updated album to conform with server refactoring of SharedAlbum to Album
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Added zoom functionality to the image viewer
|
||||||
|
* Fixed issue with the user is logged out after turning off the app
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Fixed WebSocket endpoint to confirm with the new settings on the server
|
||||||
@@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||||
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583
|
distributionSha256Sum=cd5c2958a107ee7f0722004a12d0f8559b4564c34daad7df06cffd4d12a426d0
|
||||||
@@ -23,6 +23,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FMDB (>= 2.7.5)
|
||||||
- Toast (4.0.0)
|
- Toast (4.0.0)
|
||||||
|
- url_launcher_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- video_player_avfoundation (0.0.1):
|
- video_player_avfoundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- wakelock (0.0.1):
|
- wakelock (0.0.1):
|
||||||
@@ -37,6 +39,7 @@ DEPENDENCIES:
|
|||||||
- 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`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
||||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||||
|
|
||||||
@@ -63,6 +66,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/ios"
|
||||||
|
url_launcher_ios:
|
||||||
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
||||||
wakelock:
|
wakelock:
|
||||||
@@ -80,6 +85,7 @@ SPEC CHECKSUMS:
|
|||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
|
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
@@ -76,5 +76,11 @@
|
|||||||
<false />
|
<false />
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true />
|
<true />
|
||||||
|
|
||||||
|
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>https</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.10.0"
|
version_number: "1.13.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
const String userInfoBox = "immichBoxUserInfo"; // Box
|
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
|
||||||
|
const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3
|
||||||
// Server endpoint
|
const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4
|
||||||
const String serverEndpointKey = 'immichBoxServerEndpoint';
|
|
||||||
|
|
||||||
// Login Info
|
// Login Info
|
||||||
const String hiveLoginInfoBox = "immichLoginInfoBox";
|
const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box
|
||||||
const String savedLoginInfoKey = "immichSavedLoginInfoKey";
|
const String savedLoginInfoKey = "immichSavedLoginInfoKey"; // Key 1
|
||||||
|
|
||||||
// Backup Info
|
// Backup Info
|
||||||
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox";
|
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox"; // Box
|
||||||
const String backupInfoKey = "immichBackupAlbumInfoKey";
|
const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1
|
||||||
|
|
||||||
|
// Github Release Info
|
||||||
|
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box
|
||||||
|
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ 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/backup/models/hive_backup_albums.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.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/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/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/release_info.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';
|
||||||
|
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
||||||
|
|
||||||
import 'constants/hive_box.dart';
|
import 'constants/hive_box.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
@@ -24,6 +28,7 @@ void main() async {
|
|||||||
await Hive.openBox(userInfoBox);
|
await Hive.openBox(userInfoBox);
|
||||||
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
||||||
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
|
await Hive.openBox(hiveGithubReleaseInfoBox);
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
@@ -35,23 +40,32 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ImmichApp extends ConsumerStatefulWidget {
|
class ImmichApp extends ConsumerStatefulWidget {
|
||||||
const ImmichApp({Key? key}) : super(key: key);
|
const ImmichApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ImmichAppState createState() => _ImmichAppState();
|
ImmichAppState createState() => ImmichAppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserver {
|
class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
debugPrint("[APP STATE] resumed");
|
debugPrint("[APP STATE] resumed");
|
||||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed;
|
ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed;
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
|
||||||
|
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
|
ref.watch(assetProvider.notifier).getAllAsset();
|
||||||
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
|
}
|
||||||
|
|
||||||
ref.watch(websocketProvider.notifier).connect();
|
ref.watch(websocketProvider.notifier).connect();
|
||||||
ref.watch(assetProvider.notifier).getAllAsset();
|
|
||||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -95,6 +109,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
home: Stack(
|
home: Stack(
|
||||||
@@ -103,10 +119,12 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
title: 'Immich',
|
title: 'Immich',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
primarySwatch: Colors.indigo,
|
primarySwatch: Colors.indigo,
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
snackBarTheme: const SnackBarThemeData(contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
|
||||||
scaffoldBackgroundColor: immichBackgroundColor,
|
scaffoldBackgroundColor: immichBackgroundColor,
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: immichBackgroundColor,
|
backgroundColor: immichBackgroundColor,
|
||||||
@@ -117,9 +135,11 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
routeInformationParser: _immichRouter.defaultRouteParser(),
|
routeInformationParser: _immichRouter.defaultRouteParser(),
|
||||||
routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
|
routerDelegate: _immichRouter.delegate(
|
||||||
|
navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
|
||||||
),
|
),
|
||||||
const ImmichLoadingOverlay(),
|
const ImmichLoadingOverlay(),
|
||||||
|
const VersionAnnouncementOverlay(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import 'package:latlong2/latlong.dart';
|
|||||||
class ExifBottomSheet extends ConsumerWidget {
|
class ExifBottomSheet extends ConsumerWidget {
|
||||||
final ImmichAssetWithExif assetDetail;
|
final ImmichAssetWithExif assetDetail;
|
||||||
|
|
||||||
const ExifBottomSheet({Key? key, required this.assetDetail}) : super(key: key);
|
const ExifBottomSheet({Key? key, required this.assetDetail})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
_buildMap() {
|
_buildMap() {
|
||||||
return (assetDetail.exifInfo!.latitude != null && assetDetail.exifInfo!.longitude != null)
|
return (assetDetail.exifInfo!.latitude != null &&
|
||||||
|
assetDetail.exifInfo!.longitude != null)
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -25,12 +27,14 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
center: LatLng(assetDetail.exifInfo!.latitude!, assetDetail.exifInfo!.longitude!),
|
center: LatLng(assetDetail.exifInfo!.latitude!,
|
||||||
|
assetDetail.exifInfo!.longitude!),
|
||||||
zoom: 16.0,
|
zoom: 16.0,
|
||||||
),
|
),
|
||||||
layers: [
|
layers: [
|
||||||
TileLayerOptions(
|
TileLayerOptions(
|
||||||
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
urlTemplate:
|
||||||
|
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
subdomains: ['a', 'b', 'c'],
|
subdomains: ['a', 'b', 'c'],
|
||||||
attributionBuilder: (_) {
|
attributionBuilder: (_) {
|
||||||
return const Text(
|
return const Text(
|
||||||
@@ -43,8 +47,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
markers: [
|
markers: [
|
||||||
Marker(
|
Marker(
|
||||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
point: LatLng(assetDetail.exifInfo!.latitude!, assetDetail.exifInfo!.longitude!),
|
point: LatLng(assetDetail.exifInfo!.latitude!,
|
||||||
builder: (ctx) => const Image(image: AssetImage('assets/location-pin.png')),
|
assetDetail.exifInfo!.longitude!),
|
||||||
|
builder: (ctx) => const Image(
|
||||||
|
image: AssetImage('assets/location-pin.png')),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -56,10 +62,14 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildLocationText() {
|
_buildLocationText() {
|
||||||
return (assetDetail.exifInfo!.city != null && assetDetail.exifInfo!.state != null)
|
return (assetDetail.exifInfo!.city != null &&
|
||||||
|
assetDetail.exifInfo!.state != null)
|
||||||
? Text(
|
? Text(
|
||||||
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[200], fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
)
|
)
|
||||||
: Container();
|
: Container();
|
||||||
}
|
}
|
||||||
@@ -131,7 +141,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"DETAILS",
|
"DETAILS",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
style:
|
||||||
|
TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -158,7 +169,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
leading: const Icon(Icons.camera),
|
leading: const Icon(Icons.camera),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
|
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
|
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
|
||||||
|
|||||||
113
mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
|
enum _RemoteImageStatus { empty, thumbnail, full }
|
||||||
|
|
||||||
|
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
|
late CachedNetworkImageProvider _imageProvider;
|
||||||
|
_RemoteImageStatus _status = _RemoteImageStatus.empty;
|
||||||
|
bool _zoomedIn = false;
|
||||||
|
|
||||||
|
static const int swipeThreshold = 100;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool allowMoving = _status == _RemoteImageStatus.full;
|
||||||
|
|
||||||
|
return PhotoView(
|
||||||
|
imageProvider: _imageProvider,
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
||||||
|
enablePanAlways: true,
|
||||||
|
scaleStateChangedCallback: _scaleStateChanged,
|
||||||
|
onScaleEnd: _onScaleListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScaleListener(BuildContext context, ScaleEndDetails details,
|
||||||
|
PhotoViewControllerValue controllerValue) {
|
||||||
|
// Disable swipe events when zoomed in
|
||||||
|
if (_zoomedIn) return;
|
||||||
|
|
||||||
|
if (controllerValue.position.dy > swipeThreshold) {
|
||||||
|
widget.onSwipeDown();
|
||||||
|
} else if (controllerValue.position.dy < -swipeThreshold) {
|
||||||
|
widget.onSwipeUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scaleStateChanged(PhotoViewScaleState state) {
|
||||||
|
_zoomedIn = state == PhotoViewScaleState.zoomedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
||||||
|
return CachedNetworkImageProvider(url,
|
||||||
|
headers: {"Authorization": widget.authToken}, cacheKey: url);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _performStateTransition(
|
||||||
|
_RemoteImageStatus newStatus, CachedNetworkImageProvider provider) {
|
||||||
|
// Transition to same status is forbidden
|
||||||
|
if (_status == newStatus) return;
|
||||||
|
// Transition full -> thumbnail is forbidden
|
||||||
|
if (_status == _RemoteImageStatus.full &&
|
||||||
|
newStatus == _RemoteImageStatus.thumbnail) return;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_status = newStatus;
|
||||||
|
_imageProvider = provider;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadImages() {
|
||||||
|
CachedNetworkImageProvider thumbnailProvider =
|
||||||
|
_authorizedImageProvider(widget.thumbnailUrl);
|
||||||
|
_imageProvider = thumbnailProvider;
|
||||||
|
|
||||||
|
thumbnailProvider
|
||||||
|
.resolve(const ImageConfiguration())
|
||||||
|
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
|
_performStateTransition(_RemoteImageStatus.thumbnail, thumbnailProvider);
|
||||||
|
}));
|
||||||
|
|
||||||
|
CachedNetworkImageProvider fullProvider =
|
||||||
|
_authorizedImageProvider(widget.imageUrl);
|
||||||
|
fullProvider
|
||||||
|
.resolve(const ImageConfiguration())
|
||||||
|
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
|
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_loadImages();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemotePhotoView extends StatefulWidget {
|
||||||
|
const RemotePhotoView(
|
||||||
|
{Key? key,
|
||||||
|
required this.thumbnailUrl,
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.authToken,
|
||||||
|
required this.onSwipeDown,
|
||||||
|
required this.onSwipeUp})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
final String thumbnailUrl;
|
||||||
|
final String imageUrl;
|
||||||
|
final String authToken;
|
||||||
|
|
||||||
|
final void Function() onSwipeDown;
|
||||||
|
final void Function() onSwipeUp;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _RemotePhotoViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -5,7 +7,10 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|||||||
|
|
||||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
const TopControlAppBar(
|
const TopControlAppBar(
|
||||||
{Key? key, required this.asset, required this.onMoreInfoPressed, required this.onDownloadPressed})
|
{Key? key,
|
||||||
|
required this.asset,
|
||||||
|
required this.onMoreInfoPressed,
|
||||||
|
required this.onDownloadPressed})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
final ImmichAsset asset;
|
final ImmichAsset asset;
|
||||||
@@ -42,9 +47,11 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
print("favorite");
|
log("favorite");
|
||||||
},
|
},
|
||||||
icon: asset.isFavorite ? const Icon(Icons.favorite_rounded) : const Icon(Icons.favorite_border_rounded),
|
icon: asset.isFavorite
|
||||||
|
? const Icon(Icons.favorite_rounded)
|
||||||
|
: const Icon(Icons.favorite_border_rounded),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
@@ -10,6 +8,7 @@ import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_stat
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
@@ -63,64 +62,19 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SwipeDetector(
|
body: SafeArea(
|
||||||
onSwipeDown: (_) {
|
|
||||||
AutoRouter.of(context).pop();
|
|
||||||
},
|
|
||||||
onSwipeUp: (_) {
|
|
||||||
showInfo();
|
|
||||||
},
|
|
||||||
child: SafeArea(
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: CachedNetworkImage(
|
child: RemotePhotoView(
|
||||||
fit: BoxFit.cover,
|
thumbnailUrl: thumbnailUrl,
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
authToken: "Bearer ${box.get(accessTokenKey)}",
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
errorWidget: (context, url, error) => ConstrainedBox(
|
onSwipeUp: () => showInfo(),
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
)
|
||||||
child: Wrap(
|
|
||||||
spacing: 32,
|
|
||||||
runSpacing: 32,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"Failed To Render Image - Possibly Corrupted Data",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
|
||||||
),
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: Text(
|
|
||||||
error.toString(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
placeholder: (context, url) {
|
|
||||||
return CachedNetworkImage(
|
|
||||||
cacheKey: thumbnailUrl,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: thumbnailUrl,
|
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
|
||||||
placeholderFadeInDuration: const Duration(milliseconds: 0),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) => Icon(
|
|
||||||
Icons.error,
|
|
||||||
color: Colors.grey[300],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
@@ -130,7 +84,6 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ class BackUpState extends Equatable {
|
|||||||
final BackUpProgressEnum backupProgress;
|
final BackUpProgressEnum backupProgress;
|
||||||
final List<String> allAssetOnDatabase;
|
final List<String> allAssetOnDatabase;
|
||||||
final double progressInPercentage;
|
final double progressInPercentage;
|
||||||
final CancelToken cancelToken;
|
final CancellationToken cancelToken;
|
||||||
final ServerInfo serverInfo;
|
final ServerInfo serverInfo;
|
||||||
|
|
||||||
/// All available albums on the device
|
/// All available albums on the device
|
||||||
@@ -43,7 +43,7 @@ class BackUpState extends Equatable {
|
|||||||
BackUpProgressEnum? backupProgress,
|
BackUpProgressEnum? backupProgress,
|
||||||
List<String>? allAssetOnDatabase,
|
List<String>? allAssetOnDatabase,
|
||||||
double? progressInPercentage,
|
double? progressInPercentage,
|
||||||
CancelToken? cancelToken,
|
CancellationToken? cancelToken,
|
||||||
ServerInfo? serverInfo,
|
ServerInfo? serverInfo,
|
||||||
List<AvailableAlbum>? availableAlbums,
|
List<AvailableAlbum>? availableAlbums,
|
||||||
Set<AssetPathEntity>? selectedBackupAlbums,
|
Set<AssetPathEntity>? selectedBackupAlbums,
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/available_album.model.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/modules/backup/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
@@ -19,7 +19,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
backupProgress: BackUpProgressEnum.idle,
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
allAssetOnDatabase: const [],
|
allAssetOnDatabase: const [],
|
||||||
progressInPercentage: 0,
|
progressInPercentage: 0,
|
||||||
cancelToken: CancelToken(),
|
cancelToken: CancellationToken(),
|
||||||
serverInfo: ServerInfo(
|
serverInfo: ServerInfo(
|
||||||
diskAvailable: "0",
|
diskAvailable: "0",
|
||||||
diskAvailableRaw: 0,
|
diskAvailableRaw: 0,
|
||||||
@@ -54,7 +54,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
removeExcludedAlbumForBackup(album);
|
removeExcludedAlbumForBackup(album);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
|
state = state
|
||||||
|
.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
|
||||||
_updateBackupAssetCount();
|
_updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +63,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
if (state.selectedBackupAlbums.contains(album)) {
|
if (state.selectedBackupAlbums.contains(album)) {
|
||||||
removeAlbumForBackup(album);
|
removeAlbumForBackup(album);
|
||||||
}
|
}
|
||||||
state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
|
state = state
|
||||||
|
.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
|
||||||
_updateBackupAssetCount();
|
_updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,16 +95,19 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
Future<void> getBackupAlbumsInfo() async {
|
Future<void> getBackupAlbumsInfo() async {
|
||||||
// Get all albums on the device
|
// Get all albums on the device
|
||||||
List<AvailableAlbum> availableAlbums = [];
|
List<AvailableAlbum> availableAlbums = [];
|
||||||
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(hasAll: true, type: RequestType.common);
|
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
||||||
|
hasAll: true, type: RequestType.common);
|
||||||
|
|
||||||
for (AssetPathEntity album in albums) {
|
for (AssetPathEntity album in albums) {
|
||||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||||
|
|
||||||
var assetList = await album.getAssetListRange(start: 0, end: album.assetCount);
|
var assetList =
|
||||||
|
await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
|
|
||||||
if (assetList.isNotEmpty) {
|
if (assetList.isNotEmpty) {
|
||||||
var thumbnailAsset = assetList.first;
|
var thumbnailAsset = assetList.first;
|
||||||
var thumbnailData = await thumbnailAsset.thumbnailDataWithSize(const ThumbnailSize(512, 512));
|
var thumbnailData = await thumbnailAsset
|
||||||
|
.thumbnailDataWithSize(const ThumbnailSize(512, 512));
|
||||||
availableAlbum = availableAlbum.copyWith(thumbnailData: thumbnailData);
|
availableAlbum = availableAlbum.copyWith(thumbnailData: thumbnailData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +118,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Put persistent storage info into local state of the app
|
// Put persistent storage info into local state of the app
|
||||||
// Get local storage on selected backup album
|
// Get local storage on selected backup album
|
||||||
Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
Box<HiveBackupAlbums> backupAlbumInfoBox =
|
||||||
|
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get(
|
HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get(
|
||||||
backupInfoKey,
|
backupInfoKey,
|
||||||
defaultValue: HiveBackupAlbums(
|
defaultValue: HiveBackupAlbums(
|
||||||
@@ -132,7 +138,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
debugPrint("First time backup setup recent album as default");
|
debugPrint("First time backup setup recent album as default");
|
||||||
|
|
||||||
// Get album that contains all assets
|
// Get album that contains all assets
|
||||||
var list = await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
|
var list = await PhotoManager.getAssetPathList(
|
||||||
|
hasAll: true, onlyAll: true, type: RequestType.common);
|
||||||
AssetPathEntity albumHasAllAssets = list.first;
|
AssetPathEntity albumHasAllAssets = list.first;
|
||||||
|
|
||||||
backupAlbumInfoBox.put(
|
backupAlbumInfoBox.put(
|
||||||
@@ -150,12 +157,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
try {
|
try {
|
||||||
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
||||||
state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
|
state = state.copyWith(
|
||||||
|
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
||||||
state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
|
state = state.copyWith(
|
||||||
|
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] Failed to generate album from id $e");
|
debugPrint("[ERROR] Failed to generate album from id $e");
|
||||||
@@ -172,21 +181,27 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
||||||
|
|
||||||
for (var album in state.selectedBackupAlbums) {
|
for (var album in state.selectedBackupAlbums) {
|
||||||
var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
|
var assets =
|
||||||
|
await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
assetsFromSelectedAlbums.addAll(assets);
|
assetsFromSelectedAlbums.addAll(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var album in state.excludedBackupAlbums) {
|
for (var album in state.excludedBackupAlbums) {
|
||||||
var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
|
var assets =
|
||||||
|
await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
assetsFromExcludedAlbums.addAll(assets);
|
assetsFromExcludedAlbums.addAll(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<AssetEntity> allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
Set<AssetEntity> allUniqueAssets =
|
||||||
List<String> allAssetOnDatabase = await _backupService.getDeviceBackupAsset();
|
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
||||||
|
List<String> allAssetOnDatabase =
|
||||||
|
await _backupService.getDeviceBackupAsset();
|
||||||
|
|
||||||
// Find asset that were backup from selected albums
|
// Find asset that were backup from selected albums
|
||||||
Set<String> selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.id));
|
Set<String> selectedAlbumsBackupAssets =
|
||||||
selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
|
Set.from(allUniqueAssets.map((e) => e.id));
|
||||||
|
selectedAlbumsBackupAssets
|
||||||
|
.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
|
||||||
|
|
||||||
if (allUniqueAssets.isEmpty) {
|
if (allUniqueAssets.isEmpty) {
|
||||||
debugPrint("No Asset On Device");
|
debugPrint("No Asset On Device");
|
||||||
@@ -225,7 +240,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// Hive database
|
/// Hive database
|
||||||
///
|
///
|
||||||
void _updatePersistentAlbumsSelection() {
|
void _updatePersistentAlbumsSelection() {
|
||||||
Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
Box<HiveBackupAlbums> backupAlbumInfoBox =
|
||||||
|
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
backupAlbumInfoBox.put(
|
backupAlbumInfoBox.put(
|
||||||
backupInfoKey,
|
backupInfoKey,
|
||||||
HiveBackupAlbums(
|
HiveBackupAlbums(
|
||||||
@@ -266,32 +282,42 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform Backup
|
// Perform Backup
|
||||||
state = state.copyWith(cancelToken: CancelToken());
|
state = state.copyWith(cancelToken: CancellationToken());
|
||||||
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken, _onAssetUploaded, _onUploadProgress);
|
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken,
|
||||||
|
_onAssetUploaded, _onUploadProgress);
|
||||||
} else {
|
} else {
|
||||||
PhotoManager.openSetting();
|
PhotoManager.openSetting();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancelBackup() {
|
void cancelBackup() {
|
||||||
state.cancelToken.cancel('Cancel Backup');
|
state.cancelToken.cancel();
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
state = state.copyWith(
|
||||||
|
backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(selectedAlbumsBackupAssetsIds: {
|
||||||
selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, deviceAssetId},
|
...state.selectedAlbumsBackupAssetsIds,
|
||||||
allAssetOnDatabase: [...state.allAssetOnDatabase, deviceAssetId]);
|
deviceAssetId
|
||||||
|
}, allAssetOnDatabase: [
|
||||||
|
...state.allAssetOnDatabase,
|
||||||
|
deviceAssetId
|
||||||
|
]);
|
||||||
|
|
||||||
if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) {
|
if (state.allUniqueAssets.length -
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
|
state.selectedAlbumsBackupAssetsIds.length ==
|
||||||
|
0) {
|
||||||
|
state = state.copyWith(
|
||||||
|
backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateServerInfo();
|
_updateServerInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onUploadProgress(int sent, int total) {
|
void _onUploadProgress(int sent, int total) {
|
||||||
state = state.copyWith(progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
|
state = state.copyWith(
|
||||||
|
progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateServerInfo() async {
|
void _updateServerInfo() async {
|
||||||
@@ -325,7 +351,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this device is enable backup by the user
|
// Check if this device is enable backup by the user
|
||||||
if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
|
if ((authState.deviceInfo.deviceId == authState.deviceId) &&
|
||||||
|
authState.deviceInfo.isAutoBackup) {
|
||||||
// check if backup is alreayd in process - then return
|
// check if backup is alreayd in process - then return
|
||||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||||
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
||||||
@@ -342,6 +369,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
final backupProvider =
|
||||||
|
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||||
return BackupNotifier(ref: ref);
|
return BackupNotifier(ref: ref);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import 'package:hive/hive.dart';
|
|||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/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';
|
||||||
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
|
||||||
import 'package:immich_mobile/utils/files_helper.dart';
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:cancellation_token_http/http.dart' as http;
|
||||||
|
|
||||||
class BackupService {
|
class BackupService {
|
||||||
final NetworkService _networkService = NetworkService();
|
final NetworkService _networkService = NetworkService();
|
||||||
@@ -26,17 +26,13 @@ class BackupService {
|
|||||||
return result.cast<String>();
|
return result.cast<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
backupAsset(Set<AssetEntity> assetList, CancelToken cancelToken, Function(String, String) singleAssetDoneCb,
|
backupAsset(Set<AssetEntity> assetList, http.CancellationToken cancelToken,
|
||||||
Function(int, int) uploadProgress) async {
|
Function(String, String) singleAssetDoneCb, Function(int, int) uploadProgress) async {
|
||||||
var dio = Dio();
|
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
|
||||||
|
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
File? file;
|
File? file;
|
||||||
|
|
||||||
MultipartFile assetRawUploadData;
|
http.MultipartFile? thumbnailUploadData;
|
||||||
MultipartFile thumbnailUploadData;
|
|
||||||
|
|
||||||
for (var entity in assetList) {
|
for (var entity in assetList) {
|
||||||
try {
|
try {
|
||||||
@@ -47,35 +43,27 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
FormData formData;
|
|
||||||
String originalFileName = await entity.titleAsync;
|
String originalFileName = await entity.titleAsync;
|
||||||
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
|
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
|
||||||
var fileExtension = p.extension(file.path);
|
var fileExtension = p.extension(file.path);
|
||||||
var mimeType = FileHelper.getMimeType(file.path);
|
var mimeType = FileHelper.getMimeType(file.path);
|
||||||
assetRawUploadData = await MultipartFile.fromFile(
|
var fileStream = file.openRead();
|
||||||
file.path,
|
var assetRawUploadData = http.MultipartFile(
|
||||||
|
"assetData",
|
||||||
|
fileStream,
|
||||||
|
file.lengthSync(),
|
||||||
filename: fileNameWithoutPath,
|
filename: fileNameWithoutPath,
|
||||||
contentType: MediaType(
|
contentType: MediaType(
|
||||||
mimeType["type"],
|
mimeType["type"],
|
||||||
mimeType["subType"],
|
mimeType["subType"],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
formData = FormData.fromMap({
|
|
||||||
'deviceAssetId': entity.id,
|
|
||||||
'deviceId': deviceId,
|
|
||||||
'assetType': _getAssetType(entity.type),
|
|
||||||
'createdAt': entity.createDateTime.toIso8601String(),
|
|
||||||
'modifiedAt': entity.modifiedDateTime.toIso8601String(),
|
|
||||||
'isFavorite': entity.isFavorite,
|
|
||||||
'fileExtension': fileExtension,
|
|
||||||
'duration': entity.videoDuration,
|
|
||||||
'assetData': [assetRawUploadData]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build thumbnail multipart data
|
// Build thumbnail multipart data
|
||||||
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
|
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
|
||||||
if (thumbnailData != null) {
|
if (thumbnailData != null) {
|
||||||
thumbnailUploadData = MultipartFile.fromBytes(
|
thumbnailUploadData = http.MultipartFile.fromBytes(
|
||||||
|
"thumbnailData",
|
||||||
List.from(thumbnailData),
|
List.from(thumbnailData),
|
||||||
filename: fileNameWithoutPath,
|
filename: fileNameWithoutPath,
|
||||||
contentType: MediaType(
|
contentType: MediaType(
|
||||||
@@ -83,39 +71,37 @@ class BackupService {
|
|||||||
"jpeg",
|
"jpeg",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send thumbnail data if it is exist
|
|
||||||
formData = FormData.fromMap({
|
|
||||||
'deviceAssetId': entity.id,
|
|
||||||
'deviceId': deviceId,
|
|
||||||
'assetType': _getAssetType(entity.type),
|
|
||||||
'createdAt': entity.createDateTime.toIso8601String(),
|
|
||||||
'modifiedAt': entity.modifiedDateTime.toIso8601String(),
|
|
||||||
'isFavorite': entity.isFavorite,
|
|
||||||
'fileExtension': fileExtension,
|
|
||||||
'duration': entity.videoDuration,
|
|
||||||
'thumbnailData': [thumbnailUploadData],
|
|
||||||
'assetData': [assetRawUploadData]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Response res = await dio.post(
|
var box = Hive.box(userInfoBox);
|
||||||
'$savedEndpoint/asset/upload',
|
|
||||||
data: formData,
|
var req = MultipartRequest('POST', Uri.parse('$savedEndpoint/asset/upload'),
|
||||||
cancelToken: cancelToken,
|
onProgress: ((bytes, totalBytes) => uploadProgress(bytes, totalBytes)));
|
||||||
onSendProgress: (sent, total) => uploadProgress(sent, total),
|
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
|
||||||
);
|
|
||||||
|
req.fields['deviceAssetId'] = entity.id;
|
||||||
|
req.fields['deviceId'] = deviceId;
|
||||||
|
req.fields['assetType'] = _getAssetType(entity.type);
|
||||||
|
req.fields['createdAt'] = entity.createDateTime.toIso8601String();
|
||||||
|
req.fields['modifiedAt'] = entity.modifiedDateTime.toIso8601String();
|
||||||
|
req.fields['isFavorite'] = entity.isFavorite.toString();
|
||||||
|
req.fields['fileExtension'] = fileExtension;
|
||||||
|
req.fields['duration'] = entity.videoDuration.toString();
|
||||||
|
|
||||||
|
if (thumbnailUploadData != null) {
|
||||||
|
req.files.add(thumbnailUploadData);
|
||||||
|
}
|
||||||
|
req.files.add(assetRawUploadData);
|
||||||
|
|
||||||
|
var res = await req.send(cancellationToken: cancelToken);
|
||||||
|
|
||||||
if (res.statusCode == 201) {
|
if (res.statusCode == 201) {
|
||||||
singleAssetDoneCb(entity.id, deviceId);
|
singleAssetDoneCb(entity.id, deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on DioError catch (e) {
|
} on http.CancelledException {
|
||||||
debugPrint("DioError backupAsset: ${e.response}");
|
debugPrint("Backup was cancelled by the user");
|
||||||
if (e.type == DioErrorType.cancel || e.type == DioErrorType.other) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("ERROR backupAsset: ${e.toString()}");
|
debugPrint("ERROR backupAsset: ${e.toString()}");
|
||||||
continue;
|
continue;
|
||||||
@@ -150,3 +136,35 @@ class BackupService {
|
|||||||
return DeviceInfoRemote.fromJson(res.toString());
|
return DeviceInfoRemote.fromJson(res.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MultipartRequest extends http.MultipartRequest {
|
||||||
|
/// Creates a new [MultipartRequest].
|
||||||
|
MultipartRequest(
|
||||||
|
String method,
|
||||||
|
Uri url, {
|
||||||
|
required this.onProgress,
|
||||||
|
}) : super(method, url);
|
||||||
|
|
||||||
|
final void Function(int bytes, int totalBytes) onProgress;
|
||||||
|
|
||||||
|
/// Freezes all mutable fields and returns a
|
||||||
|
/// single-subscription [http.ByteStream]
|
||||||
|
/// that will emit the request body.
|
||||||
|
@override
|
||||||
|
http.ByteStream finalize() {
|
||||||
|
final byteStream = super.finalize();
|
||||||
|
|
||||||
|
final total = contentLength;
|
||||||
|
var bytes = 0;
|
||||||
|
|
||||||
|
final t = StreamTransformer.fromHandlers(
|
||||||
|
handleData: (List<int> data, EventSink<List<int>> sink) {
|
||||||
|
bytes += data.length;
|
||||||
|
onProgress.call(bytes, total);
|
||||||
|
sink.add(data);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final stream = byteStream.transform(t);
|
||||||
|
return http.ByteStream(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,16 +17,21 @@ 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.allUniqueAssets.length -
|
||||||
backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 ? false : true;
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success');
|
ref
|
||||||
|
.watch(websocketProvider.notifier)
|
||||||
|
.stopListenToEvent('on_upload_success');
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -48,7 +53,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: LinearPercentIndicator(
|
child: LinearPercentIndicator(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
barRadius: const Radius.circular(2),
|
barRadius: const Radius.circular(2),
|
||||||
lineHeight: 6.0,
|
lineHeight: 6.0,
|
||||||
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
||||||
@@ -58,7 +64,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
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'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -67,9 +74,11 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ListTile _buildBackupController() {
|
ListTile _buildBackupController() {
|
||||||
var backUpOption = _authenticationState.deviceInfo.isAutoBackup ? "on" : "off";
|
var backUpOption =
|
||||||
var isAutoBackup = _authenticationState.deviceInfo.isAutoBackup;
|
authenticationState.deviceInfo.isAutoBackup ? "on" : "off";
|
||||||
var backupBtnText = _authenticationState.deviceInfo.isAutoBackup ? "off" : "on";
|
var isAutoBackup = authenticationState.deviceInfo.isAutoBackup;
|
||||||
|
var backupBtnText =
|
||||||
|
authenticationState.deviceInfo.isAutoBackup ? "off" : "on";
|
||||||
return ListTile(
|
return ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
leading: isAutoBackup
|
leading: isAutoBackup
|
||||||
@@ -96,12 +105,23 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: const BorderSide(
|
||||||
|
width: 1,
|
||||||
|
color: Color.fromARGB(255, 220, 220, 220),
|
||||||
|
),
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
isAutoBackup
|
isAutoBackup
|
||||||
? ref.watch(authenticationProvider.notifier).setAutoBackup(false)
|
? ref
|
||||||
: ref.watch(authenticationProvider.notifier).setAutoBackup(true);
|
.watch(authenticationProvider.notifier)
|
||||||
|
.setAutoBackup(false)
|
||||||
|
: ref
|
||||||
|
.watch(authenticationProvider.notifier)
|
||||||
|
.setAutoBackup(true);
|
||||||
},
|
},
|
||||||
child: Text("Turn $backupBtnText Backup", style: const TextStyle(fontWeight: FontWeight.bold)),
|
child: Text("Turn $backupBtnText Backup",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -127,7 +147,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -135,7 +158,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"None selected",
|
"None selected",
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -154,7 +180,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(color: Colors.red[300], fontSize: 12, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.red[300],
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -175,7 +204,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
borderOnForeground: false,
|
borderOnForeground: false,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
minVerticalPadding: 15,
|
minVerticalPadding: 15,
|
||||||
title: const Text("Backup Albums", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
title: const Text("Backup Albums",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -191,6 +221,13 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: OutlinedButton(
|
trailing: OutlinedButton(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
enableFeedback: true,
|
||||||
|
side: const BorderSide(
|
||||||
|
width: 1,
|
||||||
|
color: Color.fromARGB(255, 220, 220, 220),
|
||||||
|
),
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
|
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
|
||||||
},
|
},
|
||||||
@@ -245,13 +282,16 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Backup",
|
title: "Backup",
|
||||||
subtitle: "Photos and videos from selected albums that are backup",
|
subtitle:
|
||||||
|
"Photos and videos from selected albums that are backup",
|
||||||
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Remainder",
|
title: "Remainder",
|
||||||
subtitle: "Photos and videos that has not been backing up from selected albums",
|
subtitle:
|
||||||
info: "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
"Photos and videos that has not been backing up from selected albums",
|
||||||
|
info:
|
||||||
|
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
_buildBackupController(),
|
_buildBackupController(),
|
||||||
@@ -276,22 +316,32 @@ 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:
|
||||||
? ElevatedButton(
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
style: ElevatedButton.styleFrom(primary: Colors.red[300]),
|
? ElevatedButton(
|
||||||
onPressed: () {
|
style: ElevatedButton.styleFrom(
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
primary: Colors.red[300],
|
||||||
},
|
onPrimary: Colors.grey[50],
|
||||||
child: const Text("Cancel"),
|
),
|
||||||
)
|
onPressed: () {
|
||||||
: ElevatedButton(
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
onPressed: shouldBackup
|
},
|
||||||
? () {
|
child: const Text("Cancel"),
|
||||||
ref.read(backupProvider.notifier).startBackupProcess();
|
)
|
||||||
}
|
: ElevatedButton(
|
||||||
: null,
|
style: ElevatedButton.styleFrom(
|
||||||
child: const Text("Start Backup"),
|
primary: Theme.of(context).primaryColor,
|
||||||
),
|
onPrimary: Colors.grey[50],
|
||||||
|
),
|
||||||
|
onPressed: shouldBackup
|
||||||
|
? () {
|
||||||
|
ref
|
||||||
|
.read(backupProvider.notifier)
|
||||||
|
.startBackupProcess();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: const Text("Start Backup"),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
|
||||||
|
|
||||||
class DisableMultiSelectButton extends ConsumerWidget {
|
class DisableMultiSelectButton extends ConsumerWidget {
|
||||||
const DisableMultiSelectButton({
|
const DisableMultiSelectButton({
|
||||||
@@ -36,7 +35,8 @@ class DisableMultiSelectButton extends ConsumerWidget {
|
|||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
selectedItemCount.toString(),
|
selectedItemCount.toString(),
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600, fontSize: 18),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
this.labelTextBuilder,
|
this.labelTextBuilder,
|
||||||
this.labelConstraints,
|
this.labelConstraints,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical),
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
scrollThumbBuilder = _thumbRRectBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
|
scrollThumbBuilder =
|
||||||
|
_thumbRRectBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
DraggableScrollbar.arrows({
|
DraggableScrollbar.arrows({
|
||||||
@@ -98,7 +99,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
this.labelTextBuilder,
|
this.labelTextBuilder,
|
||||||
this.labelConstraints,
|
this.labelConstraints,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical),
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
scrollThumbBuilder = _thumbArrowBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
|
scrollThumbBuilder =
|
||||||
|
_thumbArrowBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
DraggableScrollbar.semicircle({
|
DraggableScrollbar.semicircle({
|
||||||
@@ -115,11 +117,12 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
this.labelTextBuilder,
|
this.labelTextBuilder,
|
||||||
this.labelConstraints,
|
this.labelConstraints,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical),
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb),
|
scrollThumbBuilder = _thumbSemicircleBuilder(
|
||||||
|
heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_DraggableScrollbarState createState() => _DraggableScrollbarState();
|
DraggableScrollbarState createState() => DraggableScrollbarState();
|
||||||
|
|
||||||
static buildScrollThumbAndLabel(
|
static buildScrollThumbAndLabel(
|
||||||
{required Widget scrollThumb,
|
{required Widget scrollThumb,
|
||||||
@@ -137,9 +140,9 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
children: [
|
children: [
|
||||||
ScrollLabel(
|
ScrollLabel(
|
||||||
animation: labelAnimation,
|
animation: labelAnimation,
|
||||||
child: labelText,
|
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
constraints: labelConstraints,
|
constraints: labelConstraints,
|
||||||
|
child: labelText,
|
||||||
),
|
),
|
||||||
scrollThumb,
|
scrollThumb,
|
||||||
],
|
],
|
||||||
@@ -154,7 +157,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
static ScrollThumbBuilder _thumbSemicircleBuilder(
|
||||||
|
double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
@@ -168,9 +172,6 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
foregroundPainter: ArrowCustomPainter(Colors.white),
|
foregroundPainter: ArrowCustomPainter(Colors.white),
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints.tight(Size(width, height)),
|
|
||||||
),
|
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(height),
|
topLeft: Radius.circular(height),
|
||||||
@@ -178,6 +179,9 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
topRight: const Radius.circular(4.0),
|
topRight: const Radius.circular(4.0),
|
||||||
bottomRight: const Radius.circular(4.0),
|
bottomRight: const Radius.circular(4.0),
|
||||||
),
|
),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints.tight(Size(width, height)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -193,7 +197,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbArrowBuilder(Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
static ScrollThumbBuilder _thumbArrowBuilder(
|
||||||
|
Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
@@ -203,6 +208,7 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
BoxConstraints? labelConstraints,
|
BoxConstraints? labelConstraints,
|
||||||
}) {
|
}) {
|
||||||
final scrollThumb = ClipPath(
|
final scrollThumb = ClipPath(
|
||||||
|
clipper: ArrowClipper(),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: height,
|
height: height,
|
||||||
width: 20.0,
|
width: 20.0,
|
||||||
@@ -213,7 +219,6 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
clipper: ArrowClipper(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return buildScrollThumbAndLabel(
|
return buildScrollThumbAndLabel(
|
||||||
@@ -228,7 +233,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbRRectBuilder(Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
static ScrollThumbBuilder _thumbRRectBuilder(
|
||||||
|
Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
@@ -239,13 +245,13 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
}) {
|
}) {
|
||||||
final scrollThumb = Material(
|
final scrollThumb = Material(
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints.tight(
|
constraints: BoxConstraints.tight(
|
||||||
Size(16.0, height),
|
Size(16.0, height),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
color: backgroundColor,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return buildScrollThumbAndLabel(
|
return buildScrollThumbAndLabel(
|
||||||
@@ -267,7 +273,8 @@ class ScrollLabel extends StatelessWidget {
|
|||||||
final Text child;
|
final Text child;
|
||||||
|
|
||||||
final BoxConstraints? constraints;
|
final BoxConstraints? constraints;
|
||||||
static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0);
|
static const BoxConstraints _defaultConstraints =
|
||||||
|
BoxConstraints.tightFor(width: 72.0, height: 28.0);
|
||||||
|
|
||||||
const ScrollLabel({
|
const ScrollLabel({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -298,7 +305,8 @@ class ScrollLabel extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
|
class DraggableScrollbarState extends State<DraggableScrollbar>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
late double _barOffset;
|
late double _barOffset;
|
||||||
late double _viewOffset;
|
late double _viewOffset;
|
||||||
late bool _isDragInProcess;
|
late bool _isDragInProcess;
|
||||||
@@ -345,7 +353,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb;
|
double get barMaxScrollExtent =>
|
||||||
|
context.size!.height - widget.heightScrollThumb;
|
||||||
|
|
||||||
double get barMinScrollExtent => 0;
|
double get barMinScrollExtent => 0;
|
||||||
|
|
||||||
@@ -362,7 +371,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
return LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
//print("LayoutBuilder constraints=$constraints");
|
//print("LayoutBuilder constraints=$constraints");
|
||||||
|
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
@@ -432,7 +442,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification is ScrollUpdateNotification || notification is OverscrollNotification) {
|
if (notification is ScrollUpdateNotification ||
|
||||||
|
notification is OverscrollNotification) {
|
||||||
if (_thumbAnimationController.status != AnimationStatus.forward) {
|
if (_thumbAnimationController.status != AnimationStatus.forward) {
|
||||||
_thumbAnimationController.forward();
|
_thumbAnimationController.forward();
|
||||||
}
|
}
|
||||||
@@ -486,7 +497,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||||||
_barOffset = barMaxScrollExtent;
|
_barOffset = barMaxScrollExtent;
|
||||||
}
|
}
|
||||||
|
|
||||||
double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
|
double viewDelta = getScrollViewDelta(
|
||||||
|
details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
|
||||||
|
|
||||||
_viewOffset = widget.controller.position.pixels + viewDelta;
|
_viewOffset = widget.controller.position.pixels + viewDelta;
|
||||||
if (_viewOffset < widget.controller.position.minScrollExtent) {
|
if (_viewOffset < widget.controller.position.minScrollExtent) {
|
||||||
@@ -566,7 +578,8 @@ class ArrowClipper extends CustomClipper<Path> {
|
|||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
|
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY);
|
path.lineTo(startPointX + arrowWidth, startPointY);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
|
path.lineTo(
|
||||||
|
startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
|
||||||
path.lineTo(startPointX, startPointY + 1.0);
|
path.lineTo(startPointX, startPointY + 1.0);
|
||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
@@ -575,7 +588,8 @@ class ArrowClipper extends CustomClipper<Path> {
|
|||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
|
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
|
||||||
path.lineTo(startPointX, startPointY);
|
path.lineTo(startPointX, startPointY);
|
||||||
path.lineTo(startPointX, startPointY - 1.0);
|
path.lineTo(startPointX, startPointY - 1.0);
|
||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
|
path.lineTo(
|
||||||
|
startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
@@ -600,7 +614,8 @@ class SlideFadeTransition extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: animation,
|
animation: animation,
|
||||||
builder: (context, child) => animation.value == 0.0 ? Container() : child!,
|
builder: (context, child) =>
|
||||||
|
animation.value == 0.0 ? Container() : child!,
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
position: Tween(
|
position: Tween(
|
||||||
begin: const Offset(0.3, 0.0),
|
begin: const Offset(0.3, 0.0),
|
||||||
|
|||||||
@@ -20,16 +20,18 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final BackUpState _backupState = ref.watch(backupProvider);
|
final BackUpState backupState = ref.watch(backupProvider);
|
||||||
bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
bool isEnableAutoBackup =
|
||||||
final ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
||||||
|
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
floating: true,
|
floating: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
snap: false,
|
snap: false,
|
||||||
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) {
|
||||||
return Stack(
|
return Stack(
|
||||||
@@ -47,7 +49,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_serverInfoState.isVersionMismatch
|
serverInfoState.isVersionMismatch
|
||||||
? Positioned(
|
? Positioned(
|
||||||
bottom: 12,
|
bottom: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
@@ -88,7 +90,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
Stack(
|
Stack(
|
||||||
alignment: AlignmentDirectional.center,
|
alignment: AlignmentDirectional.center,
|
||||||
children: [
|
children: [
|
||||||
_backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
? Positioned(
|
? Positioned(
|
||||||
top: 10,
|
top: 10,
|
||||||
right: 12,
|
right: 12,
|
||||||
@@ -97,7 +99,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
width: 8,
|
width: 8,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Theme.of(context).primaryColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -105,11 +108,11 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
iconSize: 30,
|
iconSize: 30,
|
||||||
icon: _isEnableAutoBackup
|
icon: isEnableAutoBackup
|
||||||
? const Icon(Icons.backup_rounded)
|
? const Icon(Icons.backup_rounded)
|
||||||
: Badge(
|
: Badge(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
elevation: 1,
|
elevation: 2,
|
||||||
position: BadgePosition.bottomEnd(bottom: -4, end: -4),
|
position: BadgePosition.bottomEnd(bottom: -4, end: -4),
|
||||||
badgeColor: Colors.white,
|
badgeColor: Colors.white,
|
||||||
badgeContent: const Icon(
|
badgeContent: const Icon(
|
||||||
@@ -117,22 +120,24 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
size: 8,
|
size: 8,
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.backup_rounded)),
|
child: const Icon(Icons.backup_rounded)),
|
||||||
tooltip: 'Backup Controller',
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
|
var onPop = await AutoRouter.of(context)
|
||||||
|
.push(const BackupControllerRoute());
|
||||||
|
|
||||||
if (onPop != null && onPop == true) {
|
if (onPop != null && onPop == true) {
|
||||||
onPopBack!();
|
onPopBack!();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
? Positioned(
|
? Positioned(
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Text(
|
child: Text(
|
||||||
(_backupState.allUniqueAssets.length - _backupState.selectedAlbumsBackupAssetsIds.length)
|
(backupState.allUniqueAssets.length -
|
||||||
|
backupState.selectedAlbumsBackupAssetsIds.length)
|
||||||
.toString(),
|
.toString(),
|
||||||
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 9, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Container()
|
: Container()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.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/modules/home/providers/upload_profile_image.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/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';
|
||||||
@@ -23,9 +24,10 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
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 uploadProfileImageStatus =
|
||||||
|
ref.watch(uploadProfileImageProvider).status;
|
||||||
final appInfo = useState({});
|
final appInfo = useState({});
|
||||||
var dummmy = Random().nextInt(1024);
|
var dummmy = Random().nextInt(1024);
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildUserProfileImage() {
|
_buildUserProfileImage() {
|
||||||
if (_authState.profileImagePath.isEmpty) {
|
if (authState.profileImagePath.isEmpty) {
|
||||||
return const CircleAvatar(
|
return const CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
@@ -48,10 +50,11 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
||||||
if (_authState.profileImagePath.isNotEmpty) {
|
if (authState.profileImagePath.isNotEmpty) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
|
backgroundImage: NetworkImage(
|
||||||
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -66,7 +69,8 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
|
backgroundImage: NetworkImage(
|
||||||
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,15 +91,16 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pickUserProfileImage() async {
|
_pickUserProfileImage() async {
|
||||||
final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
|
final XFile? image = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
var success =
|
||||||
|
await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
ref
|
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
|
||||||
.watch(authenticationProvider.notifier)
|
ref.read(uploadProfileImageProvider).profileImagePath);
|
||||||
.updateUserProfileImagePath(ref.read(uploadProfileImageProvider).profileImagePath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +121,10 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
DrawerHeader(
|
DrawerHeader(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [Color.fromARGB(255, 216, 219, 238), Color.fromARGB(255, 226, 230, 231)],
|
colors: [
|
||||||
|
Color.fromARGB(255, 216, 219, 238),
|
||||||
|
Color.fromARGB(255, 226, 230, 231)
|
||||||
|
],
|
||||||
begin: Alignment.centerRight,
|
begin: Alignment.centerRight,
|
||||||
end: Alignment.centerLeft,
|
end: Alignment.centerLeft,
|
||||||
),
|
),
|
||||||
@@ -153,21 +161,17 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Padding(padding: EdgeInsets.all(8)),
|
|
||||||
Text(
|
Text(
|
||||||
"${_authState.firstName} ${_authState.lastName}",
|
"${authState.firstName} ${authState.lastName}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Text(
|
||||||
padding: const EdgeInsets.only(top: 4.0),
|
authState.userEmail,
|
||||||
child: Text(
|
style: TextStyle(color: Colors.grey[800], fontSize: 12),
|
||||||
_authState.userEmail,
|
|
||||||
style: TextStyle(color: Colors.grey[800], fontSize: 12),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -180,16 +184,21 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Sign Out",
|
"Sign Out",
|
||||||
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.black54,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
bool res = await ref.read(authenticationProvider.notifier).logout();
|
bool res =
|
||||||
|
await ref.read(authenticationProvider.notifier).logout();
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
ref.watch(backupProvider.notifier).cancelBackup();
|
ref.watch(backupProvider.notifier).cancelBackup();
|
||||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
ref.watch(websocketProvider.notifier).disconnect();
|
ref.watch(websocketProvider.notifier).disconnect();
|
||||||
AutoRouter.of(context).popUntilRoot();
|
// AutoRouter.of(context).popUntilRoot();
|
||||||
|
AutoRouter.of(context).replace(const LoginRoute());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -208,19 +217,22 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
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(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
_serverInfoState.isVersionMismatch
|
serverInfoState.isVersionMismatch
|
||||||
? _serverInfoState.versionMismatchErrorMessage
|
? serverInfoState.versionMismatchErrorMessage
|
||||||
: "Client and Server are up-to-date",
|
: "Client and Server are up-to-date",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style:
|
style: TextStyle(
|
||||||
TextStyle(fontSize: 11, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w600),
|
fontSize: 11,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@@ -258,7 +270,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"${_serverInfoState.serverVersion.major}.${_serverInfoState.serverVersion.minor}.${_serverInfoState.serverVersion.patch}",
|
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ class HomePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
ScrollController _scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
||||||
List<Widget> _imageGridGroup = [];
|
List<Widget> imageGridGroup = [];
|
||||||
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
var isMultiSelectEnable =
|
||||||
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
var homePageState = ref.watch(homePageStateProvider);
|
var homePageState = ref.watch(homePageStateProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
@@ -39,7 +40,8 @@ class HomePage extends HookConsumerWidget {
|
|||||||
_buildSelectedItemCountIndicator() {
|
_buildSelectedItemCountIndicator() {
|
||||||
return isMultiSelectEnable
|
return isMultiSelectEnable
|
||||||
? DisableMultiSelectButton(
|
? DisableMultiSelectButton(
|
||||||
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
|
onPressed:
|
||||||
|
ref.watch(homePageStateProvider.notifier).disableMultiSelect,
|
||||||
selectedItemCount: homePageState.selectedItems.length,
|
selectedItemCount: homePageState.selectedItems.length,
|
||||||
)
|
)
|
||||||
: Container();
|
: Container();
|
||||||
@@ -59,7 +61,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (lastMonth != null) {
|
if (lastMonth != null) {
|
||||||
if (currentMonth - lastMonth! != 0) {
|
if (currentMonth - lastMonth! != 0) {
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
MonthlyTitleText(
|
MonthlyTitleText(
|
||||||
isoDate: dateGroup,
|
isoDate: dateGroup,
|
||||||
),
|
),
|
||||||
@@ -67,14 +69,14 @@ class HomePage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
DailyTitleText(
|
DailyTitleText(
|
||||||
isoDate: dateGroup,
|
isoDate: dateGroup,
|
||||||
assetGroup: immichAssetList,
|
assetGroup: immichAssetList,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
ImageGrid(assetGroup: immichAssetList),
|
ImageGrid(assetGroup: immichAssetList),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -109,12 +111,12 @@ class HomePage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 50.0),
|
padding: const EdgeInsets.only(top: 50.0),
|
||||||
child: DraggableScrollbar.semicircle(
|
child: DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
..._imageGridGroup,
|
...imageGridGroup,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
enableFeedback: true,
|
enableFeedback: true,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Save login",
|
"Stay logged in",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
),
|
),
|
||||||
value: isSaveLoginInfo.value,
|
value: isSaveLoginInfo.value,
|
||||||
@@ -96,12 +96,20 @@ class ServerEndpointInput extends StatelessWidget {
|
|||||||
|
|
||||||
const ServerEndpointInput({Key? key, required this.controller}) : super(key: key);
|
const ServerEndpointInput({Key? key, required this.controller}) : super(key: key);
|
||||||
|
|
||||||
|
String? _validateInput(String? url) {
|
||||||
|
if (url == null) return null;
|
||||||
|
if (!url.startsWith(RegExp(r'https?://'))) return 'Please specify http:// or https://';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Server Endpoint URL', border: OutlineInputBorder(), hintText: 'http://your-server-ip:port'),
|
labelText: 'Server Endpoint URL', border: OutlineInputBorder(), hintText: 'http://your-server-ip:port'),
|
||||||
|
validator: _validateInput,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,12 +119,22 @@ class EmailInput extends StatelessWidget {
|
|||||||
|
|
||||||
const EmailInput({Key? key, required this.controller}) : super(key: key);
|
const EmailInput({Key? key, required this.controller}) : super(key: key);
|
||||||
|
|
||||||
|
String? _validateInput(String? email) {
|
||||||
|
if (email == null || email == '') return null;
|
||||||
|
if (email.endsWith(' ')) return 'Trailing whitespace';
|
||||||
|
if (email.startsWith(' ')) return 'Leading whitespace';
|
||||||
|
if (email.contains(' ') || !email.contains('@')) return 'Invalid Email';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration:
|
decoration:
|
||||||
const InputDecoration(labelText: 'Email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
|
const InputDecoration(labelText: 'Email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
|
||||||
|
validator: _validateInput,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,9 +171,12 @@ class LoginButton extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
style: ButtonStyle(
|
style: ElevatedButton.styleFrom(
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(vertical: 10, horizontal: 25)),
|
primary: Theme.of(context).primaryColor,
|
||||||
|
onPrimary: Colors.grey[50],
|
||||||
|
elevation: 2,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// This will remove current cache asset state of previous user login.
|
// This will remove current cache asset state of previous user login.
|
||||||
|
|||||||
@@ -45,20 +45,23 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getSuggestedSearchTerms() async {
|
void getSuggestedSearchTerms() async {
|
||||||
var userSuggestedSearchTerms = await _searchService.getUserSuggestedSearchTerms();
|
var userSuggestedSearchTerms =
|
||||||
|
await _searchService.getUserSuggestedSearchTerms();
|
||||||
|
|
||||||
state = state.copyWith(userSuggestedSearchTerms: userSuggestedSearchTerms);
|
state = state.copyWith(userSuggestedSearchTerms: userSuggestedSearchTerms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
|
final searchPageStateProvider =
|
||||||
|
StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
|
||||||
return SearchPageStateNotifier();
|
return SearchPageStateNotifier();
|
||||||
});
|
});
|
||||||
|
|
||||||
final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
|
final getCuratedLocationProvider =
|
||||||
final SearchService _searchService = SearchService();
|
FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
|
||||||
|
final SearchService searchService = SearchService();
|
||||||
|
|
||||||
var curatedLocation = await _searchService.getCuratedLocation();
|
var curatedLocation = await searchService.getCuratedLocation();
|
||||||
if (curatedLocation != null) {
|
if (curatedLocation != null) {
|
||||||
return curatedLocation;
|
return curatedLocation;
|
||||||
} else {
|
} else {
|
||||||
@@ -66,10 +69,11 @@ final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocati
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final getCuratedObjectProvider = FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
|
final getCuratedObjectProvider =
|
||||||
final SearchService _searchService = SearchService();
|
FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
|
||||||
|
final SearchService searchService = SearchService();
|
||||||
|
|
||||||
var curatedObject = await _searchService.getCuratedObjects();
|
var curatedObject = await searchService.getCuratedObjects();
|
||||||
if (curatedObject != null) {
|
if (curatedObject != null) {
|
||||||
return curatedObject;
|
return curatedObject;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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:flutter_spinkit/flutter_spinkit.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
|||||||
@@ -12,24 +12,27 @@ import 'package:immich_mobile/modules/search/providers/search_result_page.provid
|
|||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
|
|
||||||
class SearchResultPage extends HookConsumerWidget {
|
class SearchResultPage extends HookConsumerWidget {
|
||||||
SearchResultPage({Key? key, required this.searchTerm}) : super(key: key);
|
const SearchResultPage({Key? key, required this.searchTerm})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
final String searchTerm;
|
final String searchTerm;
|
||||||
late FocusNode searchFocusNode;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
ScrollController _scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
final searchTermController = useTextEditingController(text: "");
|
final searchTermController = useTextEditingController(text: "");
|
||||||
final isNewSearch = useState(false);
|
final isNewSearch = useState(false);
|
||||||
final currentSearchTerm = useState(searchTerm);
|
final currentSearchTerm = useState(searchTerm);
|
||||||
|
|
||||||
List<Widget> _imageGridGroup = [];
|
final List<Widget> imageGridGroup = [];
|
||||||
|
|
||||||
|
late FocusNode searchFocusNode;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
searchFocusNode = FocusNode();
|
searchFocusNode = FocusNode();
|
||||||
|
|
||||||
Future.delayed(Duration.zero, () => ref.read(searchResultPageProvider.notifier).search(searchTerm));
|
Future.delayed(Duration.zero,
|
||||||
|
() => ref.read(searchResultPageProvider.notifier).search(searchTerm));
|
||||||
return () => searchFocusNode.dispose();
|
return () => searchFocusNode.dispose();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -79,14 +82,17 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
return Chip(
|
return Chip(
|
||||||
label: Wrap(
|
label: Wrap(
|
||||||
spacing: 5,
|
spacing: 5,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Text(
|
||||||
padding: const EdgeInsets.only(top: 2.0),
|
currentSearchTerm.value,
|
||||||
child: Text(
|
style: TextStyle(
|
||||||
currentSearchTerm.value,
|
color: Theme.of(context).primaryColor,
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor),
|
fontSize: 13,
|
||||||
maxLines: 1,
|
fontWeight: FontWeight.bold),
|
||||||
),
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
Icons.close_rounded,
|
Icons.close_rounded,
|
||||||
@@ -124,7 +130,7 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (lastMonth != null) {
|
if (lastMonth != null) {
|
||||||
if (currentMonth - lastMonth! != 0) {
|
if (currentMonth - lastMonth! != 0) {
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
MonthlyTitleText(
|
MonthlyTitleText(
|
||||||
isoDate: dateGroup,
|
isoDate: dateGroup,
|
||||||
),
|
),
|
||||||
@@ -132,14 +138,14 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
DailyTitleText(
|
DailyTitleText(
|
||||||
isoDate: dateGroup,
|
isoDate: dateGroup,
|
||||||
assetGroup: immichAssetList,
|
assetGroup: immichAssetList,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
ImageGrid(assetGroup: immichAssetList),
|
ImageGrid(assetGroup: immichAssetList),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -148,11 +154,11 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return DraggableScrollbar.semicircle(
|
return DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
slivers: [..._imageGridGroup],
|
slivers: [...imageGridGroup],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -192,7 +198,9 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildSearchResult(),
|
_buildSearchResult(),
|
||||||
isNewSearch.value ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
|
isNewSearch.value
|
||||||
|
? SearchSuggestionList(onSubmitted: _onSearchSubmitted)
|
||||||
|
: Container(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_user.model.dart';
|
import 'package:immich_mobile/shared/models/user.model.dart';
|
||||||
|
|
||||||
class SharedAlbum {
|
class SharedAlbum {
|
||||||
final String id;
|
final String id;
|
||||||
@@ -11,8 +11,8 @@ class SharedAlbum {
|
|||||||
final String albumName;
|
final String albumName;
|
||||||
final String createdAt;
|
final String createdAt;
|
||||||
final String? albumThumbnailAssetId;
|
final String? albumThumbnailAssetId;
|
||||||
final List<SharedUsers> sharedUsers;
|
final List<User> sharedUsers;
|
||||||
final List<SharedAssets>? sharedAssets;
|
final List<ImmichAsset>? assets;
|
||||||
|
|
||||||
SharedAlbum({
|
SharedAlbum({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -21,7 +21,7 @@ class SharedAlbum {
|
|||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.albumThumbnailAssetId,
|
required this.albumThumbnailAssetId,
|
||||||
required this.sharedUsers,
|
required this.sharedUsers,
|
||||||
this.sharedAssets,
|
this.assets,
|
||||||
});
|
});
|
||||||
|
|
||||||
SharedAlbum copyWith({
|
SharedAlbum copyWith({
|
||||||
@@ -30,8 +30,8 @@ class SharedAlbum {
|
|||||||
String? albumName,
|
String? albumName,
|
||||||
String? createdAt,
|
String? createdAt,
|
||||||
String? albumThumbnailAssetId,
|
String? albumThumbnailAssetId,
|
||||||
List<SharedUsers>? sharedUsers,
|
List<User>? sharedUsers,
|
||||||
List<SharedAssets>? sharedAssets,
|
List<ImmichAsset>? assets,
|
||||||
}) {
|
}) {
|
||||||
return SharedAlbum(
|
return SharedAlbum(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -40,7 +40,7 @@ class SharedAlbum {
|
|||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
albumThumbnailAssetId: albumThumbnailAssetId ?? this.albumThumbnailAssetId,
|
albumThumbnailAssetId: albumThumbnailAssetId ?? this.albumThumbnailAssetId,
|
||||||
sharedUsers: sharedUsers ?? this.sharedUsers,
|
sharedUsers: sharedUsers ?? this.sharedUsers,
|
||||||
sharedAssets: sharedAssets ?? this.sharedAssets,
|
assets: assets ?? this.assets,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ class SharedAlbum {
|
|||||||
result.addAll({'albumThumbnailAssetId': albumThumbnailAssetId});
|
result.addAll({'albumThumbnailAssetId': albumThumbnailAssetId});
|
||||||
}
|
}
|
||||||
result.addAll({'sharedUsers': sharedUsers.map((x) => x.toMap()).toList()});
|
result.addAll({'sharedUsers': sharedUsers.map((x) => x.toMap()).toList()});
|
||||||
if (sharedAssets != null) {
|
if (assets != null) {
|
||||||
result.addAll({'sharedAssets': sharedAssets!.map((x) => x.toMap()).toList()});
|
result.addAll({'assets': assets!.map((x) => x.toMap()).toList()});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -69,9 +69,9 @@ class SharedAlbum {
|
|||||||
albumName: map['albumName'] ?? '',
|
albumName: map['albumName'] ?? '',
|
||||||
createdAt: map['createdAt'] ?? '',
|
createdAt: map['createdAt'] ?? '',
|
||||||
albumThumbnailAssetId: map['albumThumbnailAssetId'],
|
albumThumbnailAssetId: map['albumThumbnailAssetId'],
|
||||||
sharedUsers: List<SharedUsers>.from(map['sharedUsers']?.map((x) => SharedUsers.fromMap(x))),
|
sharedUsers: List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))),
|
||||||
sharedAssets: map['sharedAssets'] != null
|
assets: map['assets'] != null
|
||||||
? List<SharedAssets>.from(map['sharedAssets']?.map((x) => SharedAssets.fromMap(x)))
|
? List<ImmichAsset>.from(map['assets']?.map((x) => ImmichAsset.fromMap(x)))
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ class SharedAlbum {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SharedAlbum(id: $id, ownerId: $ownerId, albumName: $albumName, createdAt: $createdAt, albumThumbnailAssetId: $albumThumbnailAssetId, sharedUsers: $sharedUsers, sharedAssets: $sharedAssets)';
|
return 'SharedAlbum(id: $id, ownerId: $ownerId, albumName: $albumName, createdAt: $createdAt, albumThumbnailAssetId: $albumThumbnailAssetId, sharedUsers: $sharedUsers, assets: $assets)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -97,7 +97,7 @@ class SharedAlbum {
|
|||||||
other.createdAt == createdAt &&
|
other.createdAt == createdAt &&
|
||||||
other.albumThumbnailAssetId == albumThumbnailAssetId &&
|
other.albumThumbnailAssetId == albumThumbnailAssetId &&
|
||||||
listEquals(other.sharedUsers, sharedUsers) &&
|
listEquals(other.sharedUsers, sharedUsers) &&
|
||||||
listEquals(other.sharedAssets, sharedAssets);
|
listEquals(other.assets, assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -108,6 +108,6 @@ class SharedAlbum {
|
|||||||
createdAt.hashCode ^
|
createdAt.hashCode ^
|
||||||
albumThumbnailAssetId.hashCode ^
|
albumThumbnailAssetId.hashCode ^
|
||||||
sharedUsers.hashCode ^
|
sharedUsers.hashCode ^
|
||||||
sharedAssets.hashCode;
|
assets.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
|
|
||||||
class SharedAssets {
|
|
||||||
final ImmichAsset assetInfo;
|
|
||||||
|
|
||||||
SharedAssets({
|
|
||||||
required this.assetInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
SharedAssets copyWith({
|
|
||||||
ImmichAsset? assetInfo,
|
|
||||||
}) {
|
|
||||||
return SharedAssets(
|
|
||||||
assetInfo: assetInfo ?? this.assetInfo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'assetInfo': assetInfo.toMap()});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory SharedAssets.fromMap(Map<String, dynamic> map) {
|
|
||||||
return SharedAssets(
|
|
||||||
assetInfo: ImmichAsset.fromMap(map['assetInfo']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory SharedAssets.fromJson(String source) => SharedAssets.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'SharedAssets(assetInfo: $assetInfo)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is SharedAssets && other.assetInfo == assetInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => assetInfo.hashCode;
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/user_info.model.dart';
|
|
||||||
|
|
||||||
class SharedUsers {
|
|
||||||
final int id;
|
|
||||||
final String albumId;
|
|
||||||
final String sharedUserId;
|
|
||||||
final UserInfo userInfo;
|
|
||||||
|
|
||||||
SharedUsers({
|
|
||||||
required this.id,
|
|
||||||
required this.albumId,
|
|
||||||
required this.sharedUserId,
|
|
||||||
required this.userInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
SharedUsers copyWith({
|
|
||||||
int? id,
|
|
||||||
String? albumId,
|
|
||||||
String? sharedUserId,
|
|
||||||
UserInfo? userInfo,
|
|
||||||
}) {
|
|
||||||
return SharedUsers(
|
|
||||||
id: id ?? this.id,
|
|
||||||
albumId: albumId ?? this.albumId,
|
|
||||||
sharedUserId: sharedUserId ?? this.sharedUserId,
|
|
||||||
userInfo: userInfo ?? this.userInfo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'id': id});
|
|
||||||
result.addAll({'albumId': albumId});
|
|
||||||
result.addAll({'sharedUserId': sharedUserId});
|
|
||||||
result.addAll({'userInfo': userInfo.toMap()});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory SharedUsers.fromMap(Map<String, dynamic> map) {
|
|
||||||
return SharedUsers(
|
|
||||||
id: map['id']?.toInt() ?? 0,
|
|
||||||
albumId: map['albumId'] ?? '',
|
|
||||||
sharedUserId: map['sharedUserId'] ?? '',
|
|
||||||
userInfo: UserInfo.fromMap(map['userInfo']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory SharedUsers.fromJson(String source) => SharedUsers.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'SharedUsers(id: $id, albumId: $albumId, sharedUserId: $sharedUserId, userInfo: $userInfo)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is SharedUsers &&
|
|
||||||
other.id == id &&
|
|
||||||
other.albumId == albumId &&
|
|
||||||
other.sharedUserId == sharedUserId &&
|
|
||||||
other.userInfo == userInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^ albumId.hashCode ^ sharedUserId.hashCode ^ userInfo.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,8 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
|||||||
final SharedAlbumService _sharedAlbumService = SharedAlbumService();
|
final SharedAlbumService _sharedAlbumService = SharedAlbumService();
|
||||||
|
|
||||||
getAllSharedAlbums() async {
|
getAllSharedAlbums() async {
|
||||||
List<SharedAlbum> sharedAlbums = await _sharedAlbumService.getAllSharedAlbum();
|
List<SharedAlbum> sharedAlbums =
|
||||||
|
await _sharedAlbumService.getAllSharedAlbum();
|
||||||
|
|
||||||
state = sharedAlbums;
|
state = sharedAlbums;
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,8 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(String albumId, List<String> assetIds) async {
|
Future<bool> removeAssetFromAlbum(
|
||||||
|
String albumId, List<String> assetIds) async {
|
||||||
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
|
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
@@ -46,12 +48,14 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final sharedAlbumProvider = StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
|
final sharedAlbumProvider =
|
||||||
|
StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
|
||||||
return SharedAlbumNotifier();
|
return SharedAlbumNotifier();
|
||||||
});
|
});
|
||||||
|
|
||||||
final sharedAlbumDetailProvider = FutureProvider.autoDispose.family<SharedAlbum, String>((ref, albumId) async {
|
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
||||||
final SharedAlbumService _sharedAlbumService = SharedAlbumService();
|
.family<SharedAlbum, String>((ref, albumId) async {
|
||||||
|
final SharedAlbumService sharedAlbumService = SharedAlbumService();
|
||||||
|
|
||||||
return await _sharedAlbumService.getAlbumDetail(albumId);
|
return await sharedAlbumService.getAlbumDetail(albumId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/user_info.model.dart';
|
import 'package:immich_mobile/shared/models/user.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/user.service.dart';
|
import 'package:immich_mobile/shared/services/user.service.dart';
|
||||||
|
|
||||||
final suggestedSharedUsersProvider = FutureProvider.autoDispose<List<UserInfo>>((ref) async {
|
final suggestedSharedUsersProvider = FutureProvider.autoDispose<List<User>>((ref) async {
|
||||||
UserService userService = UserService();
|
UserService userService = UserService();
|
||||||
|
|
||||||
return await userService.getAllUsersInfo();
|
return await userService.getAllUsersInfo();
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ class SharedAlbumService {
|
|||||||
|
|
||||||
Future<List<SharedAlbum>> getAllSharedAlbum() async {
|
Future<List<SharedAlbum>> getAllSharedAlbum() async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: 'shared/allSharedAlbums');
|
var res = await _networkService.getRequest(url: 'album?shared=true');
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||||
List<SharedAlbum> result = List.from(decodedData.map((e) => SharedAlbum.fromMap(e)));
|
List<SharedAlbum> result =
|
||||||
|
List.from(decodedData.map((e) => SharedAlbum.fromMap(e)));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -24,9 +25,10 @@ class SharedAlbumService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> createSharedAlbum(String albumName, Set<ImmichAsset> assets, List<String> sharedUserIds) async {
|
Future<bool> createSharedAlbum(String albumName, Set<ImmichAsset> assets,
|
||||||
|
List<String> sharedUserIds) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.postRequest(url: 'shared/createAlbum', data: {
|
var res = await _networkService.postRequest(url: 'album', data: {
|
||||||
"albumName": albumName,
|
"albumName": albumName,
|
||||||
"sharedWithUserIds": sharedUserIds,
|
"sharedWithUserIds": sharedUserIds,
|
||||||
"assetIds": assets.map((asset) => asset.id).toList(),
|
"assetIds": assets.map((asset) => asset.id).toList(),
|
||||||
@@ -45,7 +47,7 @@ class SharedAlbumService {
|
|||||||
|
|
||||||
Future<SharedAlbum> getAlbumDetail(String albumId) async {
|
Future<SharedAlbum> getAlbumDetail(String albumId) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: 'shared/$albumId');
|
var res = await _networkService.getRequest(url: 'album/$albumId');
|
||||||
dynamic decodedData = jsonDecode(res.toString());
|
dynamic decodedData = jsonDecode(res.toString());
|
||||||
SharedAlbum result = SharedAlbum.fromMap(decodedData);
|
SharedAlbum result = SharedAlbum.fromMap(decodedData);
|
||||||
|
|
||||||
@@ -55,9 +57,11 @@ class SharedAlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addAdditionalAssetToAlbum(Set<ImmichAsset> assets, String albumId) async {
|
Future<bool> addAdditionalAssetToAlbum(
|
||||||
|
Set<ImmichAsset> assets, String albumId) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.postRequest(url: 'shared/addAssets', data: {
|
var res =
|
||||||
|
await _networkService.putRequest(url: 'album/$albumId/assets', data: {
|
||||||
"albumId": albumId,
|
"albumId": albumId,
|
||||||
"assetIds": assets.map((asset) => asset.id).toList(),
|
"assetIds": assets.map((asset) => asset.id).toList(),
|
||||||
});
|
});
|
||||||
@@ -73,10 +77,11 @@ class SharedAlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addAdditionalUserToAlbum(List<String> sharedUserIds, String albumId) async {
|
Future<bool> addAdditionalUserToAlbum(
|
||||||
|
List<String> sharedUserIds, String albumId) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.postRequest(url: 'shared/addUsers', data: {
|
var res =
|
||||||
"albumId": albumId,
|
await _networkService.putRequest(url: 'album/$albumId/users', data: {
|
||||||
"sharedUserIds": sharedUserIds,
|
"sharedUserIds": sharedUserIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,7 +98,7 @@ class SharedAlbumService {
|
|||||||
|
|
||||||
Future<bool> deleteAlbum(String albumId) async {
|
Future<bool> deleteAlbum(String albumId) async {
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.deleteRequest(url: 'shared/$albumId');
|
Response res = await _networkService.deleteRequest(url: 'album/$albumId');
|
||||||
|
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
return false;
|
return false;
|
||||||
@@ -108,7 +113,8 @@ class SharedAlbumService {
|
|||||||
|
|
||||||
Future<bool> leaveAlbum(String albumId) async {
|
Future<bool> leaveAlbum(String albumId) async {
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.deleteRequest(url: 'shared/leaveAlbum/$albumId');
|
Response res =
|
||||||
|
await _networkService.deleteRequest(url: 'album/$albumId/user/me');
|
||||||
|
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
return false;
|
return false;
|
||||||
@@ -121,10 +127,11 @@ class SharedAlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(String albumId, List<String> assetIds) async {
|
Future<bool> removeAssetFromAlbum(
|
||||||
|
String albumId, List<String> assetIds) async {
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.deleteRequest(url: 'shared/removeAssets/', data: {
|
Response res = await _networkService
|
||||||
"albumId": albumId,
|
.deleteRequest(url: 'album/$albumId/assets', data: {
|
||||||
"assetIds": assetIds,
|
"assetIds": assetIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -139,10 +146,11 @@ class SharedAlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> changeTitleAlbum(String albumId, String ownerId, String newAlbumTitle) async {
|
Future<bool> changeTitleAlbum(
|
||||||
|
String albumId, String ownerId, String newAlbumTitle) async {
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.patchRequest(url: 'shared/updateInfo', data: {
|
Response res =
|
||||||
"albumId": albumId,
|
await _networkService.patchRequest(url: 'album/$albumId/', data: {
|
||||||
"ownerId": ownerId,
|
"ownerId": ownerId,
|
||||||
"albumName": newAlbumTitle,
|
"albumName": newAlbumTitle,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,15 +13,14 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
style: ButtonStyle(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(vertical: 0, horizontal: 10)),
|
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
|
||||||
shape: MaterialStateProperty.resolveWith<OutlinedBorder>(
|
shape: RoundedRectangleBorder(
|
||||||
(_) => RoundedRectangleBorder(
|
borderRadius: BorderRadius.circular(25),
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
side: MaterialStateProperty.resolveWith<BorderSide>(
|
side: const BorderSide(
|
||||||
(_) => const BorderSide(width: 1, color: Color.fromARGB(255, 158, 158, 158)),
|
width: 1,
|
||||||
|
color: Color.fromARGB(255, 215, 215, 215),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
icon: Icon(iconData, size: 15),
|
icon: Icon(iconData, size: 15),
|
||||||
|
|||||||
@@ -26,18 +26,22 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
|
final isMultiSelectionEnable =
|
||||||
final selectedAssetsInAlbum = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
ref.watch(assetSelectionProvider).isMultiselectEnable;
|
||||||
|
final selectedAssetsInAlbum =
|
||||||
|
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||||
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||||
|
|
||||||
void _onDeleteAlbumPressed(String albumId) async {
|
void _onDeleteAlbumPressed(String albumId) async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
|
bool isSuccess =
|
||||||
|
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
|
AutoRouter.of(context)
|
||||||
|
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -53,10 +57,12 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
void _onLeaveAlbumPressed(String albumId) async {
|
void _onLeaveAlbumPressed(String albumId) async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(albumId);
|
bool isSuccess =
|
||||||
|
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
|
AutoRouter.of(context)
|
||||||
|
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -73,10 +79,11 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
void _onRemoveFromAlbumPressed(String albumId) async {
|
void _onRemoveFromAlbumPressed(String albumId) async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
bool isSuccess =
|
||||||
albumId,
|
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
||||||
selectedAssetsInAlbum.map((a) => a.id).toList(),
|
albumId,
|
||||||
);
|
selectedAssetsInAlbum.map((a) => a.id).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -153,15 +160,18 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
_buildLeadingButton() {
|
_buildLeadingButton() {
|
||||||
if (isMultiSelectionEnable) {
|
if (isMultiSelectionEnable) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () => ref.watch(assetSelectionProvider.notifier).disableMultiselection(),
|
onPressed: () => ref
|
||||||
|
.watch(assetSelectionProvider.notifier)
|
||||||
|
.disableMultiselection(),
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
);
|
);
|
||||||
} else if (isEditAlbum) {
|
} else if (isEditAlbum) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
bool isSuccess =
|
bool isSuccess = await ref
|
||||||
await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(albumId, userId, newAlbumTitle);
|
.watch(albumViewerProvider.notifier)
|
||||||
|
.changeAlbumTitle(albumId, userId, newAlbumTitle);
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -187,7 +197,9 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
return AppBar(
|
return AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: _buildLeadingButton(),
|
leading: _buildLeadingButton(),
|
||||||
title: isMultiSelectionEnable ? Text(selectedAssetsInAlbum.length.toString()) : Container(),
|
title: isMultiSelectionEnable
|
||||||
|
? Text(selectedAssetsInAlbum.length.toString())
|
||||||
|
: Container(),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@@ -86,63 +86,60 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Hero(
|
child: Stack(
|
||||||
tag: asset.id,
|
children: [
|
||||||
child: Stack(
|
Container(
|
||||||
children: [
|
decoration: BoxDecoration(border: drawBorderColor()),
|
||||||
Container(
|
child: CachedNetworkImage(
|
||||||
decoration: BoxDecoration(border: drawBorderColor()),
|
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||||
child: CachedNetworkImage(
|
width: 150,
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
height: 150,
|
||||||
width: 150,
|
memCacheHeight: asset.type == 'IMAGE' ? 150 : 150,
|
||||||
height: 150,
|
fit: BoxFit.cover,
|
||||||
memCacheHeight: asset.type == 'IMAGE' ? 150 : 150,
|
imageUrl: thumbnailRequestUrl,
|
||||||
fit: BoxFit.cover,
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
imageUrl: thumbnailRequestUrl,
|
fadeInDuration: const Duration(milliseconds: 250),
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
scale: 0.2,
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
|
child: CircularProgressIndicator(value: downloadProgress.progress),
|
||||||
scale: 0.2,
|
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
errorWidget: (context, url, error) {
|
||||||
|
return Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.all(3.0),
|
Padding(
|
||||||
child: Align(
|
padding: const EdgeInsets.all(3.0),
|
||||||
alignment: Alignment.topLeft,
|
child: Align(
|
||||||
child: _buildSelectionIcon(asset),
|
alignment: Alignment.topLeft,
|
||||||
),
|
child: _buildSelectionIcon(asset),
|
||||||
),
|
),
|
||||||
asset.type == 'IMAGE'
|
),
|
||||||
? Container()
|
asset.type == 'IMAGE'
|
||||||
: Positioned(
|
? Container()
|
||||||
bottom: 5,
|
: Positioned(
|
||||||
right: 5,
|
bottom: 5,
|
||||||
child: Row(
|
right: 5,
|
||||||
children: [
|
child: Row(
|
||||||
Text(
|
children: [
|
||||||
asset.duration.toString().substring(0, 7),
|
Text(
|
||||||
style: const TextStyle(
|
asset.duration.toString().substring(0, 7),
|
||||||
color: Colors.white,
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(
|
|
||||||
Icons.play_circle_outline_rounded,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const Icon(
|
||||||
)
|
Icons.play_circle_outline_rounded,
|
||||||
],
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,32 +23,29 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
// debugPrint("View ${asset.id}");
|
// debugPrint("View ${asset.id}");
|
||||||
},
|
},
|
||||||
child: Hero(
|
child: Stack(
|
||||||
tag: asset.id,
|
children: [
|
||||||
child: Stack(
|
CachedNetworkImage(
|
||||||
children: [
|
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||||
CachedNetworkImage(
|
width: 500,
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
height: 500,
|
||||||
width: 500,
|
memCacheHeight: asset.type == 'IMAGE' ? 500 : 500,
|
||||||
height: 500,
|
fit: BoxFit.cover,
|
||||||
memCacheHeight: asset.type == 'IMAGE' ? 500 : 500,
|
imageUrl: thumbnailRequestUrl,
|
||||||
fit: BoxFit.cover,
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
imageUrl: thumbnailRequestUrl,
|
fadeInDuration: const Duration(milliseconds: 250),
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
scale: 0.2,
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
|
child: CircularProgressIndicator(value: downloadProgress.progress),
|
||||||
scale: 0.2,
|
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
errorWidget: (context, url, error) {
|
||||||
),
|
return Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"Create shared album",
|
"Create shared album",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -69,7 +69,7 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"Share with partner",
|
"Share with partner",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -28,32 +28,33 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
FocusNode titleFocusNode = useFocusNode();
|
FocusNode titleFocusNode = useFocusNode();
|
||||||
ScrollController _scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
AsyncValue<SharedAlbum> _albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
|
AsyncValue<SharedAlbum> albumInfo =
|
||||||
|
ref.watch(sharedAlbumDetailProvider(albumId));
|
||||||
|
|
||||||
final userId = ref.watch(authenticationProvider).userId;
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
|
|
||||||
/// Find out if the assets in album exist on the device
|
/// Find out if the assets in album exist on the device
|
||||||
/// If they exist, add to selected asset state to show they are already selected.
|
/// If they exist, add to selected asset state to show they are already selected.
|
||||||
void _onAddPhotosPressed(SharedAlbum albumInfo) async {
|
void _onAddPhotosPressed(SharedAlbum albumInfo) async {
|
||||||
if (albumInfo.sharedAssets != null && albumInfo.sharedAssets!.isNotEmpty) {
|
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) {
|
||||||
ref
|
ref
|
||||||
.watch(assetSelectionProvider.notifier)
|
.watch(assetSelectionProvider.notifier)
|
||||||
.addNewAssets(albumInfo.sharedAssets!.map((e) => e.assetInfo).toList());
|
.addNewAssets(albumInfo.assets!.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
||||||
|
|
||||||
AssetSelectionPageResult? returnPayload =
|
AssetSelectionPageResult? returnPayload = await AutoRouter.of(context)
|
||||||
await AutoRouter.of(context).push<AssetSelectionPageResult?>(const AssetSelectionRoute());
|
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
|
||||||
|
|
||||||
if (returnPayload != null) {
|
if (returnPayload != null) {
|
||||||
// Check if there is new assets add
|
// Check if there is new assets add
|
||||||
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
|
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
var isSuccess =
|
var isSuccess = await SharedAlbumService().addAdditionalAssetToAlbum(
|
||||||
await SharedAlbumService().addAdditionalAssetToAlbum(returnPayload.selectedAdditionalAsset, albumId);
|
returnPayload.selectedAdditionalAsset, albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
ref.refresh(sharedAlbumDetailProvider(albumId));
|
ref.refresh(sharedAlbumDetailProvider(albumId));
|
||||||
@@ -69,13 +70,15 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onAddUsersPressed(SharedAlbum albumInfo) async {
|
void _onAddUsersPressed(SharedAlbum albumInfo) async {
|
||||||
List<String>? sharedUserIds =
|
List<String>? sharedUserIds = await AutoRouter.of(context)
|
||||||
await AutoRouter.of(context).push<List<String>?>(SelectAdditionalUserForSharingRoute(albumInfo: albumInfo));
|
.push<List<String>?>(
|
||||||
|
SelectAdditionalUserForSharingRoute(albumInfo: albumInfo));
|
||||||
|
|
||||||
if (sharedUserIds != null) {
|
if (sharedUserIds != null) {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
var isSuccess = await SharedAlbumService().addAdditionalUserToAlbum(sharedUserIds, albumId);
|
var isSuccess = await SharedAlbumService()
|
||||||
|
.addAdditionalUserToAlbum(sharedUserIds, albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
ref.refresh(sharedAlbumDetailProvider(albumId));
|
ref.refresh(sharedAlbumDetailProvider(albumId));
|
||||||
@@ -95,16 +98,20 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Text(albumInfo.albumName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
child: Text(albumInfo.albumName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24, fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAlbumDateRange(SharedAlbum albumInfo) {
|
Widget _buildAlbumDateRange(SharedAlbum albumInfo) {
|
||||||
if (albumInfo.sharedAssets != null && albumInfo.sharedAssets!.isNotEmpty) {
|
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) {
|
||||||
String startDate = "";
|
String startDate = "";
|
||||||
DateTime parsedStartDate = DateTime.parse(albumInfo.sharedAssets!.first.assetInfo.createdAt);
|
DateTime parsedStartDate =
|
||||||
DateTime parsedEndDate = DateTime.parse(albumInfo.sharedAssets!.last.assetInfo.createdAt);
|
DateTime.parse(albumInfo.assets!.first.createdAt);
|
||||||
|
DateTime parsedEndDate =
|
||||||
|
DateTime.parse(albumInfo.assets!.last.createdAt);
|
||||||
|
|
||||||
if (parsedStartDate.year == parsedEndDate.year) {
|
if (parsedStartDate.year == parsedEndDate.year) {
|
||||||
startDate = DateFormat('LLL d').format(parsedStartDate);
|
startDate = DateFormat('LLL d').format(parsedStartDate);
|
||||||
@@ -118,7 +125,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(left: 16.0, top: 8),
|
padding: const EdgeInsets.only(left: 16.0, top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
"$startDate-$endDate",
|
"$startDate-$endDate",
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
|
style: const TextStyle(
|
||||||
|
fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -147,8 +155,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(2.0),
|
padding: const EdgeInsets.all(2.0),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
child: Image.asset('assets/immich-logo-no-outline.png'),
|
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
|
child:
|
||||||
|
Image.asset('assets/immich-logo-no-outline.png'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -163,7 +172,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImageGrid(SharedAlbum albumInfo) {
|
Widget _buildImageGrid(SharedAlbum albumInfo) {
|
||||||
if (albumInfo.sharedAssets != null && albumInfo.sharedAssets!.isNotEmpty) {
|
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) {
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
sliver: SliverGrid(
|
sliver: SliverGrid(
|
||||||
@@ -174,9 +183,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return AlbumViewerThumbnail(asset: albumInfo.sharedAssets![index].assetInfo);
|
return AlbumViewerThumbnail(asset: albumInfo.assets![index]);
|
||||||
},
|
},
|
||||||
childCount: albumInfo.sharedAssets?.length,
|
childCount: albumInfo.assets?.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -217,10 +226,10 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: DraggableScrollbar.semicircle(
|
child: DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
_buildHeader(albumInfo),
|
_buildHeader(albumInfo),
|
||||||
SliverPersistentHeader(
|
SliverPersistentHeader(
|
||||||
@@ -242,8 +251,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AlbumViewerAppbar(albumInfo: _albumInfo, userId: userId, albumId: albumId),
|
appBar: AlbumViewerAppbar(
|
||||||
body: _albumInfo.when(
|
albumInfo: albumInfo, userId: userId, albumId: albumId),
|
||||||
|
body: albumInfo.when(
|
||||||
data: (albumInfo) => _buildBody(albumInfo),
|
data: (albumInfo) => _buildBody(albumInfo),
|
||||||
error: (e, _) => Center(child: Text("Error loading album info $e")),
|
error: (e, _) => Center(child: Text("Error loading album info $e")),
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
|
|||||||