Compare commits
103 Commits
v1.31.0_49
...
v1.33.0_52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25848b78f9 | ||
|
|
f94176a910 | ||
|
|
ae96508e15 | ||
|
|
95ebf815eb | ||
|
|
b713fb5650 | ||
|
|
6159c83fd2 | ||
|
|
f1af17bf4d | ||
|
|
a87c1c1210 | ||
|
|
e63d165b65 | ||
|
|
9411770253 | ||
|
|
dc80ac1c88 | ||
|
|
bb055628cc | ||
|
|
390bcdb8c6 | ||
|
|
d95bcb46ad | ||
|
|
7b954e21e7 | ||
|
|
a6eea4d096 | ||
|
|
2c189d5c78 | ||
|
|
85a80fd032 | ||
|
|
0309b47515 | ||
|
|
95d8f60389 | ||
|
|
1ec7122381 | ||
|
|
061b229e12 | ||
|
|
3617433858 | ||
|
|
d6d525cc1b | ||
|
|
e752290458 | ||
|
|
d77e25425e | ||
|
|
028c0249a3 | ||
|
|
a3ca5307a5 | ||
|
|
6796462b13 | ||
|
|
d08475d5af | ||
|
|
d310c77fc8 | ||
|
|
75d8ca1306 | ||
|
|
894eea739e | ||
|
|
1156290377 | ||
|
|
c271f0c224 | ||
|
|
a7f14dc103 | ||
|
|
f05d5bdb9e | ||
|
|
e99c400f59 | ||
|
|
e38166837d | ||
|
|
d43a08eb71 | ||
|
|
293e713af6 | ||
|
|
03866b4c31 | ||
|
|
4f2c08525f | ||
|
|
2c12f53937 | ||
|
|
c88e5f9be2 | ||
|
|
0f51a9794e | ||
|
|
edd1f49e57 | ||
|
|
4df0cf2d07 | ||
|
|
87ba99755b | ||
|
|
c03f860f8e | ||
|
|
f2e0e3f345 | ||
|
|
fee652dfd7 | ||
|
|
839446a88d | ||
|
|
028b8c8bcc | ||
|
|
64b1d4ca3b | ||
|
|
c6cbee6563 | ||
|
|
a406f6e7cc | ||
|
|
9869b92c2b | ||
|
|
00549eed79 | ||
|
|
0c4968dc30 | ||
|
|
704335c898 | ||
|
|
ec74feea5a | ||
|
|
2f5cc3059a | ||
|
|
4355485581 | ||
|
|
342c3254cb | ||
|
|
5fc82dfaa2 | ||
|
|
6ab6507db9 | ||
|
|
3c807ae86e | ||
|
|
9bfacaa39a | ||
|
|
a2882a4908 | ||
|
|
1adc64a352 | ||
|
|
c28863966b | ||
|
|
14dc679332 | ||
|
|
17085dd8a0 | ||
|
|
82b8313da0 | ||
|
|
4f7e764fa0 | ||
|
|
d52da8bbea | ||
|
|
cdddcad784 | ||
|
|
38767cad0f | ||
|
|
c3d7dda61f | ||
|
|
c4e32ce159 | ||
|
|
6355a07dc4 | ||
|
|
0e3fb41e73 | ||
|
|
fdac5af5ee | ||
|
|
0e509ceafa | ||
|
|
6b84534632 | ||
|
|
fc255b558d | ||
|
|
9e54e30011 | ||
|
|
77312ce2e0 | ||
|
|
9a6d29d6e7 | ||
|
|
2cb7517f64 | ||
|
|
3228882fc0 | ||
|
|
6804e3dc73 | ||
|
|
f9af61a5ca | ||
|
|
a94b443f13 | ||
|
|
fd06aa2135 | ||
|
|
dd0f40559d | ||
|
|
a117e897ca | ||
|
|
347ac70063 | ||
|
|
50842ef815 | ||
|
|
1970a64f6f | ||
|
|
dd71a53f5e | ||
|
|
8440d9890c |
24
.github/workflows/build_push_docker_latest.yml
vendored
24
.github/workflows/build_push_docker_latest.yml
vendored
@@ -17,17 +17,17 @@ jobs:
|
|||||||
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.1.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.2.1
|
||||||
- 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 Immich Mono Repo
|
- name: Build and push Immich Mono Repo
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -45,17 +45,17 @@ jobs:
|
|||||||
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.1.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.2.1
|
||||||
- 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 Machine Learning
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./machine-learning
|
context: ./machine-learning
|
||||||
file: ./machine-learning/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
@@ -72,17 +72,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
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.1.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.2.1
|
||||||
- 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 Web
|
- name: Build and Push Web
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
@@ -100,17 +100,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
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.1.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.2.1
|
||||||
- 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 Proxy
|
- name: Build and Push Proxy
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
file: ./nginx/Dockerfile
|
file: ./nginx/Dockerfile
|
||||||
|
|||||||
24
.github/workflows/build_push_docker_staging.yml
vendored
24
.github/workflows/build_push_docker_staging.yml
vendored
@@ -17,10 +17,10 @@ jobs:
|
|||||||
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.1.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.2.1
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ github.repository == 'immich-app/immich' }}
|
if: ${{ github.repository == 'immich-app/immich' }}
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push Immich Mono Repo
|
- name: Build and push Immich Mono Repo
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -47,10 +47,10 @@ jobs:
|
|||||||
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.1.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.2.1
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ github.repository == 'immich-app/immich' }}
|
if: ${{ github.repository == 'immich-app/immich' }}
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Machine Learning
|
- name: Build and Push Machine Learning
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./machine-learning
|
context: ./machine-learning
|
||||||
file: ./machine-learning/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
@@ -76,10 +76,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
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.1.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.2.1
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ github.repository == 'immich-app/immich' }}
|
if: ${{ github.repository == 'immich-app/immich' }}
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Web
|
- name: Build and Push Web
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
@@ -106,10 +106,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
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.1.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.2.1
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ github.repository == 'immich-app/immich' }}
|
if: ${{ github.repository == 'immich-app/immich' }}
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and Push Proxy
|
- name: Build and Push Proxy
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
file: ./nginx/Dockerfile
|
file: ./nginx/Dockerfile
|
||||||
|
|||||||
24
.github/workflows/build_push_server_release.yml
vendored
24
.github/workflows/build_push_server_release.yml
vendored
@@ -22,11 +22,11 @@ jobs:
|
|||||||
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.1.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.2.1
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push immich-server release
|
- name: Build and push immich-server release
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -58,17 +58,17 @@ jobs:
|
|||||||
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.1.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.2.1
|
||||||
- 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 Machine Learning
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./machine-learning
|
context: ./machine-learning
|
||||||
file: ./machine-learning/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
@@ -94,11 +94,11 @@ jobs:
|
|||||||
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.1.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.2.1
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push immich-web release
|
- name: Build and push immich-web release
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
@@ -134,11 +134,11 @@ jobs:
|
|||||||
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.1.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.2.1
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push immich-proxy release
|
- name: Build and push immich-proxy release
|
||||||
uses: docker/build-push-action@v3.1.1
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
file: ./nginx/Dockerfile
|
file: ./nginx/Dockerfile
|
||||||
|
|||||||
21
.github/workflows/dispatch_sdk_update.yml
vendored
Normal file
21
.github/workflows/dispatch_sdk_update.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Update Immich SDK
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-sdk-repos:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GH_TOKEN }}
|
||||||
|
script: |
|
||||||
|
await github.rest.actions.createWorkflowDispatch({
|
||||||
|
owner: 'immich-app',
|
||||||
|
repo: 'immich-sdk-typescript-axios',
|
||||||
|
workflow_id: 'build.yml',
|
||||||
|
ref: 'main'
|
||||||
|
})
|
||||||
17
README.md
17
README.md
@@ -53,7 +53,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
|||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development, there will be continuous functions, features and api changes.
|
> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development. There will be continuous functions, features and api changes.
|
||||||
|
|
||||||
| Features | Mobile | Web |
|
| Features | Mobile | Web |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
@@ -116,13 +116,12 @@ There are several services that compose Immich:
|
|||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
NOTE: When using a reverse proxy in front of Immich (such as NGINX), the reverse proxy might require extra configuration to allow large files to be uploaded (such as client_max_body_size in the case of NGINX).
|
NOTE: When using a reverse proxy in front of Immich (such as NGINX), the reverse proxy might require extra configuration to allow large files to be uploaded (such as `client_max_body_size` in the case of NGINX).
|
||||||
|
## Testing one-step installation (not recommended for production)
|
||||||
|
|
||||||
## Testing One-step installation (not recommended for production)
|
> ⚠️ *This installation method is for evaluating Immich before further customization to meet the users' needs.*
|
||||||
|
|
||||||
> ⚠️ *This installation method is for evaluating Immich before futher customization to meet the users' needs.*
|
*Applicable operating systems: Ubuntu, Debian, MacOS*
|
||||||
|
|
||||||
*Applicable system: Ubuntu, Debian, MacOS*
|
|
||||||
|
|
||||||
- In the shell, from the directory of your choice, run the following command:
|
- In the shell, from the directory of your choice, run the following command:
|
||||||
|
|
||||||
@@ -197,6 +196,10 @@ If you have installed, you can update the application by navigate to the directo
|
|||||||
```bash
|
```bash
|
||||||
docker-compose pull && docker-compose up -d
|
docker-compose pull && docker-compose up -d
|
||||||
```
|
```
|
||||||
|
# Unraid Installation
|
||||||
|
|
||||||
|
Please follow this [article](https://mfaz.dev/posts/immich-unraid/) for a tutorial on how to install Immich on Unraid
|
||||||
|
|
||||||
|
|
||||||
# Mobile app
|
# Mobile app
|
||||||
|
|
||||||
@@ -204,7 +207,7 @@ docker-compose pull && docker-compose up -d
|
|||||||
| - | - | - |
|
| - | - | - |
|
||||||
| <a href="https://f-droid.org/packages/app.alextran.immich"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> | <p align="left"> <a href="https://play.google.com/store/apps/details?id=app.alextran.immich"><img src="design/google-play-qr-code.png" width="200" title="Google Play Store"></a> <p/> | <p align="left"> <a href="https://apps.apple.com/us/app/immich/id1613945652"><img src="design/ios-qr-code.png" width="200" title="Apple App Store"></a> <p/> |
|
| <a href="https://f-droid.org/packages/app.alextran.immich"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> | <p align="left"> <a href="https://play.google.com/store/apps/details?id=app.alextran.immich"><img src="design/google-play-qr-code.png" width="200" title="Google Play Store"></a> <p/> | <p align="left"> <a href="https://apps.apple.com/us/app/immich/id1613945652"><img src="design/ios-qr-code.png" width="200" title="Apple App Store"></a> <p/> |
|
||||||
|
|
||||||
> *The Play/App Store version might be lagging behind the latest release due to the review process.*
|
> *The Play/App Store version might be lagging behind the latest release due to their review process.*
|
||||||
|
|
||||||
# App Beta release channel
|
# App Beta release channel
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ LOG_LEVEL=simple
|
|||||||
# JWT SECRET
|
# JWT SECRET
|
||||||
###################################################################################
|
###################################################################################
|
||||||
|
|
||||||
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
# This JWT_SECRET is used to sign the authentication keys for user login
|
||||||
|
# You should set it to a long randomly generated value
|
||||||
|
# You can use this command to generate one: openssl rand -base64 128
|
||||||
|
JWT_SECRET=
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# Reverse Geocoding
|
# Reverse Geocoding
|
||||||
|
|||||||
33
install.sh
33
install.sh
@@ -18,33 +18,37 @@ get_release_version() {
|
|||||||
create_immich_directory() {
|
create_immich_directory() {
|
||||||
echo "Creating Immich directory..."
|
echo "Creating Immich directory..."
|
||||||
mkdir -p ./immich-app/immich-data
|
mkdir -p ./immich-app/immich-data
|
||||||
|
cd ./immich-app
|
||||||
}
|
}
|
||||||
|
|
||||||
download_docker_compose_file() {
|
download_docker_compose_file() {
|
||||||
echo "Downloading docker-compose.yml..."
|
echo "Downloading docker-compose.yml..."
|
||||||
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
|
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./docker-compose.yml >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
download_dot_env_file() {
|
download_dot_env_file() {
|
||||||
echo "Downloading .env file..."
|
echo "Downloading .env file..."
|
||||||
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
|
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./.env >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_env_value() {
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
sed -i '' "s|$1=.*|$1=$2|" ./.env
|
||||||
|
else
|
||||||
|
sed -i "s|$1=.*|$1=$2|" ./.env
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
populate_upload_location() {
|
populate_upload_location() {
|
||||||
echo "Populating default UPLOAD_LOCATION value..."
|
echo "Populating default UPLOAD_LOCATION value..."
|
||||||
|
upload_location=$(pwd)/immich-data
|
||||||
|
replace_env_value "UPLOAD_LOCATION" $upload_location
|
||||||
|
}
|
||||||
|
|
||||||
cd ./immich-app/immich-data
|
generate_jwt_secret() {
|
||||||
|
echo "Generating JWT_SECRET value..."
|
||||||
upload_location=$(pwd)
|
jwt_secret=$(openssl rand -base64 128)
|
||||||
|
replace_env_value "JWT_SECRET" $jwt_secret
|
||||||
# Replace value of UPLOAD_LOCATION in .env with upload_location path
|
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
||||||
sed -i '' "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
|
|
||||||
else
|
|
||||||
sed -i "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start_docker_compose() {
|
start_docker_compose() {
|
||||||
@@ -88,4 +92,5 @@ create_immich_directory
|
|||||||
download_docker_compose_file
|
download_docker_compose_file
|
||||||
download_dot_env_file
|
download_dot_env_file
|
||||||
populate_upload_location
|
populate_upload_location
|
||||||
|
generate_jwt_secret
|
||||||
start_docker_compose
|
start_docker_compose
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update > /dev/null \
|
||||||
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
&& apt-get install --no-install-recommends -y gcc g++ make cmake python3 python3-pip ffmpeg > /dev/null \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
|
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
|
||||||
@@ -23,6 +24,9 @@ FROM node:16-bullseye-slim
|
|||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
COPY LICENSE /licenses/LICENSE.txt
|
||||||
|
COPY LICENSE /LICENSE
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
@@ -30,13 +34,18 @@ COPY entrypoint.sh ./
|
|||||||
|
|
||||||
RUN mkdir -p /usr/src/app/dist \
|
RUN mkdir -p /usr/src/app/dist \
|
||||||
&& mkdir -p /usr/src/app/node_modules \
|
&& mkdir -p /usr/src/app/node_modules \
|
||||||
&& apt-get update \
|
&& mkdir -p /usr/src/app/.reverse-geocoding-dump \
|
||||||
&& apt-get install -y ffmpeg \
|
&& apt-get update > /dev/null \
|
||||||
&& rm -rf /var/cache/apt/lists
|
&& apt-get install --no-install-recommends -y ffmpeg > /dev/null \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/cache/apt/lists/*
|
||||||
|
|
||||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||||
COPY --from=builder /usr/src/app/dist ./dist
|
COPY --from=builder /usr/src/app/dist ./dist
|
||||||
|
|
||||||
RUN npm prune --production
|
RUN npm prune --production
|
||||||
|
|
||||||
# CMD [ "node", "dist/main" ]
|
RUN chown -R node:0 /usr/src/app \
|
||||||
|
&& chmod -R g=u /usr/src/app
|
||||||
|
|
||||||
|
RUN addgroup node root
|
||||||
21
machine-learning/LICENSE
Normal file
21
machine-learning/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Hau Tran
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 49,
|
"android.injected.version.code" => 52,
|
||||||
"android.injected.version.name" => "1.31.0",
|
"android.injected.version.name" => "1.33.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Integrate new grid system to the main timeline.
|
||||||
|
* Minor UI update.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Improved slow initial loading for large amount of asset.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
* Fixed back button navigation
|
||||||
|
* Added duplicated asset upload handling mechanism
|
||||||
|
* Fixed cannot signout completely when "Save Logged In" is checked
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"backup_controller_page_backup_selected": "Ausgewählt: ",
|
"backup_controller_page_backup_selected": "Ausgewählt: ",
|
||||||
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
|
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
|
||||||
"backup_controller_page_cancel": "Abbrechen",
|
"backup_controller_page_cancel": "Abbrechen",
|
||||||
|
"backup_background_service_default_notification": "Suche nach neuen assets…",
|
||||||
"backup_controller_page_created": "Erstellt: {}",
|
"backup_controller_page_created": "Erstellt: {}",
|
||||||
"backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.",
|
"backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.",
|
||||||
"backup_controller_page_excluded": "Ausgeschlossen: ",
|
"backup_controller_page_excluded": "Ausgeschlossen: ",
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"backup_controller_page_backup_sub": "Backed up photos and videos",
|
"backup_controller_page_backup_sub": "Backed up photos and videos",
|
||||||
"backup_controller_page_cancel": "Cancel",
|
"backup_controller_page_cancel": "Cancel",
|
||||||
"backup_controller_page_created": "Created on: {}",
|
"backup_controller_page_created": "Created on: {}",
|
||||||
"backup_controller_page_desc_backup": "Turn on backup to automatically upload new assets to the server.",
|
"backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.",
|
||||||
"backup_controller_page_excluded": "Excluded: ",
|
"backup_controller_page_excluded": "Excluded: ",
|
||||||
"backup_controller_page_failed": "Failed ({})",
|
"backup_controller_page_failed": "Failed ({})",
|
||||||
"backup_controller_page_filename": "File name: {} [{}]",
|
"backup_controller_page_filename": "File name: {} [{}]",
|
||||||
@@ -58,14 +58,14 @@
|
|||||||
"backup_controller_page_select": "Select",
|
"backup_controller_page_select": "Select",
|
||||||
"backup_controller_page_server_storage": "Server Storage",
|
"backup_controller_page_server_storage": "Server Storage",
|
||||||
"backup_controller_page_start_backup": "Start Backup",
|
"backup_controller_page_start_backup": "Start Backup",
|
||||||
"backup_controller_page_status_off": "Backup is off",
|
"backup_controller_page_status_off": "Automatic foreground backup is off",
|
||||||
"backup_controller_page_status_on": "Backup is on",
|
"backup_controller_page_status_on": "Automatic foreground backup is on",
|
||||||
"backup_controller_page_storage_format": "{} of {} used",
|
"backup_controller_page_storage_format": "{} of {} used",
|
||||||
"backup_controller_page_to_backup": "Albums to be backup",
|
"backup_controller_page_to_backup": "Albums to be backup",
|
||||||
"backup_controller_page_total": "Total",
|
"backup_controller_page_total": "Total",
|
||||||
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
|
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
|
||||||
"backup_controller_page_turn_off": "Turn off Backup",
|
"backup_controller_page_turn_off": "Turn off foreground backup",
|
||||||
"backup_controller_page_turn_on": "Turn on Backup",
|
"backup_controller_page_turn_on": "Turn on foreground backup",
|
||||||
"backup_controller_page_uploading_file_info": "Uploading file info",
|
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||||
"backup_err_only_album": "Cannot remove the only album",
|
"backup_err_only_album": "Cannot remove the only album",
|
||||||
"backup_info_card_assets": "assets",
|
"backup_info_card_assets": "assets",
|
||||||
@@ -171,8 +171,5 @@
|
|||||||
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
||||||
"experimental_settings_title": "Experimental",
|
"experimental_settings_title": "Experimental",
|
||||||
"experimental_settings_subtitle": "Use at your own risk!",
|
"experimental_settings_subtitle": "Use at your own risk!"
|
||||||
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
|
||||||
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
|
||||||
"settings_require_restart": "Please restart Immich to apply this setting"
|
|
||||||
}
|
}
|
||||||
@@ -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.31.0"
|
version_number: "1.33.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -25,3 +25,7 @@ const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
|
|||||||
const String backupFailedSince = "immichBackupFailedSince"; // Key 1
|
const String backupFailedSince = "immichBackupFailedSince"; // Key 1
|
||||||
const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2
|
const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2
|
||||||
const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3
|
const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3
|
||||||
|
|
||||||
|
// Duplicate asset
|
||||||
|
const String duplicatedAssetsBox = "immichDuplicatedAssetsBox"; // Box
|
||||||
|
const String duplicatedAssetsKey = "immichDuplicatedAssetsKey"; // Key 1
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
|
||||||
import 'package:immich_mobile/constants/locales.dart';
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
import 'package:immich_mobile/modules/backup/background_service/background.service.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/models/hive_duplicated_assets.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.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/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
@@ -31,12 +31,14 @@ void main() async {
|
|||||||
|
|
||||||
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
||||||
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
||||||
|
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
|
||||||
|
|
||||||
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);
|
await Hive.openBox(hiveGithubReleaseInfoBox);
|
||||||
await Hive.openBox(userSettingInfoBox);
|
await Hive.openBox(userSettingInfoBox);
|
||||||
|
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
AlbumNotifier(this._albumService) : super([]);
|
AlbumNotifier(this._albumService, this._albumCacheService) : super([]);
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
|
final AlbumCacheService _albumCacheService;
|
||||||
|
|
||||||
|
_cacheState() {
|
||||||
|
_albumCacheService.put(state);
|
||||||
|
}
|
||||||
|
|
||||||
getAllAlbums() async {
|
getAllAlbums() async {
|
||||||
|
|
||||||
|
if (await _albumCacheService.isValid() && state.isEmpty) {
|
||||||
|
state = await _albumCacheService.get();
|
||||||
|
}
|
||||||
|
|
||||||
List<AlbumResponseDto>? albums =
|
List<AlbumResponseDto>? albums =
|
||||||
await _albumService.getAlbums(isShared: false);
|
await _albumService.getAlbums(isShared: false);
|
||||||
|
|
||||||
if (albums != null) {
|
if (albums != null) {
|
||||||
state = albums;
|
state = albums;
|
||||||
|
_cacheState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAlbum(String albumId) {
|
deleteAlbum(String albumId) {
|
||||||
state = state.where((album) => album.id != albumId).toList();
|
state = state.where((album) => album.id != albumId).toList();
|
||||||
|
_cacheState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AlbumResponseDto?> createAlbum(
|
Future<AlbumResponseDto?> createAlbum(
|
||||||
@@ -28,6 +41,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
|||||||
|
|
||||||
if (album != null) {
|
if (album != null) {
|
||||||
state = [...state, album];
|
state = [...state, album];
|
||||||
|
_cacheState();
|
||||||
|
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -36,5 +51,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
|||||||
|
|
||||||
final albumProvider =
|
final albumProvider =
|
||||||
StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
|
StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
|
||||||
return AlbumNotifier(ref.watch(albumServiceProvider));
|
return AlbumNotifier(
|
||||||
|
ref.watch(albumServiceProvider),
|
||||||
|
ref.watch(albumCacheServiceProvider),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
|
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]);
|
||||||
|
|
||||||
final AlbumService _sharedAlbumService;
|
final AlbumService _sharedAlbumService;
|
||||||
|
final SharedAlbumCacheService _sharedAlbumCacheService;
|
||||||
|
|
||||||
|
_cacheState() {
|
||||||
|
_sharedAlbumCacheService.put(state);
|
||||||
|
}
|
||||||
|
|
||||||
Future<AlbumResponseDto?> createSharedAlbum(
|
Future<AlbumResponseDto?> createSharedAlbum(
|
||||||
String albumName,
|
String albumName,
|
||||||
@@ -22,6 +28,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
|||||||
|
|
||||||
if (newAlbum != null) {
|
if (newAlbum != null) {
|
||||||
state = [...state, newAlbum];
|
state = [...state, newAlbum];
|
||||||
|
_cacheState();
|
||||||
}
|
}
|
||||||
|
|
||||||
return newAlbum;
|
return newAlbum;
|
||||||
@@ -33,16 +40,22 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllSharedAlbums() async {
|
getAllSharedAlbums() async {
|
||||||
|
if (await _sharedAlbumCacheService.isValid() && state.isEmpty) {
|
||||||
|
state = await _sharedAlbumCacheService.get();
|
||||||
|
}
|
||||||
|
|
||||||
List<AlbumResponseDto>? sharedAlbums =
|
List<AlbumResponseDto>? sharedAlbums =
|
||||||
await _sharedAlbumService.getAlbums(isShared: true);
|
await _sharedAlbumService.getAlbums(isShared: true);
|
||||||
|
|
||||||
if (sharedAlbums != null) {
|
if (sharedAlbums != null) {
|
||||||
state = sharedAlbums;
|
state = sharedAlbums;
|
||||||
|
_cacheState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAlbum(String albumId) async {
|
deleteAlbum(String albumId) async {
|
||||||
state = state.where((album) => album.id != albumId).toList();
|
state = state.where((album) => album.id != albumId).toList();
|
||||||
|
_cacheState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> leaveAlbum(String albumId) async {
|
Future<bool> leaveAlbum(String albumId) async {
|
||||||
@@ -50,6 +63,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
|||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
state = state.where((album) => album.id != albumId).toList();
|
state = state.where((album) => album.id != albumId).toList();
|
||||||
|
_cacheState();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
@@ -72,7 +86,10 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
|||||||
|
|
||||||
final sharedAlbumProvider =
|
final sharedAlbumProvider =
|
||||||
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
|
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
|
||||||
return SharedAlbumNotifier(ref.watch(albumServiceProvider));
|
return SharedAlbumNotifier(
|
||||||
|
ref.watch(albumServiceProvider),
|
||||||
|
ref.watch(sharedAlbumCacheServiceProvider),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
||||||
|
|||||||
49
mobile/lib/modules/album/services/album_cache.service.dart
Normal file
49
mobile/lib/modules/album/services/album_cache.service.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/json_cache.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
class BaseAlbumCacheService extends JsonCache<List<AlbumResponseDto>> {
|
||||||
|
BaseAlbumCacheService(super.cacheFileName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void put(List<AlbumResponseDto> data) {
|
||||||
|
putRawData(data.map((e) => e.toJson()).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<AlbumResponseDto>> get() async {
|
||||||
|
try {
|
||||||
|
final mapList = await readRawData() as List<dynamic>;
|
||||||
|
|
||||||
|
final responseData = mapList
|
||||||
|
.map((e) => AlbumResponseDto.fromJson(e))
|
||||||
|
.whereNotNull()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(e.toString());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlbumCacheService extends BaseAlbumCacheService {
|
||||||
|
AlbumCacheService() : super("album_cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
class SharedAlbumCacheService extends BaseAlbumCacheService {
|
||||||
|
SharedAlbumCacheService() : super("shared_album_cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
final albumCacheServiceProvider = Provider(
|
||||||
|
(ref) => AlbumCacheService(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final sharedAlbumCacheServiceProvider = Provider(
|
||||||
|
(ref) => SharedAlbumCacheService(),
|
||||||
|
);
|
||||||
|
|
||||||
@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
primary: Theme.of(context).primaryColor,
|
foregroundColor: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
onPressed:
|
onPressed:
|
||||||
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
|
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
|
||||||
|
|||||||
@@ -1,5 +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/services.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:flutter_swipe_detector/flutter_swipe_detector.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
@@ -111,6 +112,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
: const BouncingScrollPhysics(),
|
: const BouncingScrollPhysics(),
|
||||||
itemCount: assetList.length,
|
itemCount: assetList.length,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
|
onPageChanged: (value) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
},
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
initState(index);
|
initState(index);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/modules/backup/background_service/localization.dar
|
|||||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/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/models/hive_duplicated_assets.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/models/hive_saved_login_info.model.dart';
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
@@ -316,10 +317,13 @@ class BackgroundService {
|
|||||||
|
|
||||||
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
||||||
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
||||||
|
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
|
||||||
|
|
||||||
await Hive.openBox(userInfoBox);
|
await Hive.openBox(userInfoBox);
|
||||||
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
||||||
await Hive.openBox(userSettingInfoBox);
|
await Hive.openBox(userSettingInfoBox);
|
||||||
await Hive.openBox(backgroundBackupInfoBox);
|
await Hive.openBox(backgroundBackupInfoBox);
|
||||||
|
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
|
||||||
|
|
||||||
ApiService apiService = ApiService();
|
ApiService apiService = ApiService();
|
||||||
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
||||||
@@ -410,7 +414,7 @@ class BackgroundService {
|
|||||||
final bool ok = await backupService.backupAsset(
|
final bool ok = await backupService.backupAsset(
|
||||||
toUpload,
|
toUpload,
|
||||||
_cancellationToken!,
|
_cancellationToken!,
|
||||||
notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId) {},
|
notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {},
|
||||||
notifySingleProgress ? _onProgress : (sent, total) {},
|
notifySingleProgress ? _onProgress : (sent, total) {},
|
||||||
notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
|
notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
|
||||||
_onBackupError,
|
_onBackupError,
|
||||||
@@ -429,7 +433,7 @@ class BackgroundService {
|
|||||||
return "$percent% ($_uploadedAssetsCount/$_assetsToUploadCount)";
|
return "$percent% ($_uploadedAssetsCount/$_assetsToUploadCount)";
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) {
|
||||||
debugPrint("Uploaded $deviceAssetId from $deviceId");
|
debugPrint("Uploaded $deviceAssetId from $deviceId");
|
||||||
_uploadedAssetsCount++;
|
_uploadedAssetsCount++;
|
||||||
_updateNotification(
|
_updateNotification(
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ Future<bool> loadTranslations() async {
|
|||||||
|
|
||||||
await controller.loadTranslations();
|
await controller.loadTranslations();
|
||||||
|
|
||||||
return Localization.load(controller.locale,
|
return Localization.load(
|
||||||
translations: controller.translations,
|
controller.locale,
|
||||||
fallbackTranslations: controller.fallbackTranslations);
|
translations: controller.translations,
|
||||||
|
fallbackTranslations: controller.fallbackTranslations,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,10 @@ class ErrorUploadAsset extends Equatable {
|
|||||||
List<Object> get props {
|
List<Object> get props {
|
||||||
return [
|
return [
|
||||||
id,
|
id,
|
||||||
|
createdAt,
|
||||||
fileName,
|
fileName,
|
||||||
fileType,
|
fileType,
|
||||||
|
asset,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
|
part 'hive_duplicated_assets.model.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: 2)
|
||||||
|
class HiveDuplicatedAssets {
|
||||||
|
@HiveField(0, defaultValue: [])
|
||||||
|
List<String> duplicatedAssetIds;
|
||||||
|
|
||||||
|
HiveDuplicatedAssets({
|
||||||
|
required this.duplicatedAssetIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
HiveDuplicatedAssets copyWith({
|
||||||
|
List<String>? duplicatedAssetIds,
|
||||||
|
}) {
|
||||||
|
return HiveDuplicatedAssets(
|
||||||
|
duplicatedAssetIds: duplicatedAssetIds ?? this.duplicatedAssetIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'duplicatedAssetIds': duplicatedAssetIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory HiveDuplicatedAssets.fromMap(Map<String, dynamic> map) {
|
||||||
|
return HiveDuplicatedAssets(
|
||||||
|
duplicatedAssetIds: List<String>.from(map['duplicatedAssetIds']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory HiveDuplicatedAssets.fromJson(String source) =>
|
||||||
|
HiveDuplicatedAssets.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'HiveDuplicatedAssets(duplicatedAssetIds: $duplicatedAssetIds)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return other is HiveDuplicatedAssets &&
|
||||||
|
listEquals(other.duplicatedAssetIds, duplicatedAssetIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => duplicatedAssetIds.hashCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'hive_duplicated_assets.model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// TypeAdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class HiveDuplicatedAssetsAdapter extends TypeAdapter<HiveDuplicatedAssets> {
|
||||||
|
@override
|
||||||
|
final int typeId = 2;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HiveDuplicatedAssets read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return HiveDuplicatedAssets(
|
||||||
|
duplicatedAssetIds:
|
||||||
|
fields[0] == null ? [] : (fields[0] as List).cast<String>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, HiveDuplicatedAssets obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(1)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.duplicatedAssetIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is HiveDuplicatedAssetsAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
|||||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/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/models/hive_duplicated_assets.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
@@ -296,6 +297,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// Those assets are unique and are used as the total assets
|
/// Those assets are unique and are used as the total assets
|
||||||
///
|
///
|
||||||
Future<void> _updateBackupAssetCount() async {
|
Future<void> _updateBackupAssetCount() async {
|
||||||
|
Set<String> duplicatedAssetIds = _backupService.getDuplicatedAssetIds();
|
||||||
Set<AssetEntity> assetsFromSelectedAlbums = {};
|
Set<AssetEntity> assetsFromSelectedAlbums = {};
|
||||||
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
||||||
|
|
||||||
@@ -326,9 +328,15 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
// Find asset that were backup from selected albums
|
// Find asset that were backup from selected albums
|
||||||
Set<String> selectedAlbumsBackupAssets =
|
Set<String> selectedAlbumsBackupAssets =
|
||||||
Set.from(allUniqueAssets.map((e) => e.id));
|
Set.from(allUniqueAssets.map((e) => e.id));
|
||||||
|
|
||||||
selectedAlbumsBackupAssets
|
selectedAlbumsBackupAssets
|
||||||
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
|
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
|
||||||
|
|
||||||
|
// Remove duplicated asset from all unique assets
|
||||||
|
allUniqueAssets.removeWhere(
|
||||||
|
(asset) => duplicatedAssetIds.contains(asset.id),
|
||||||
|
);
|
||||||
|
|
||||||
if (allUniqueAssets.isEmpty) {
|
if (allUniqueAssets.isEmpty) {
|
||||||
debugPrint("No Asset On Device");
|
debugPrint("No Asset On Device");
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@@ -455,14 +463,26 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
void _onAssetUploaded(
|
||||||
state = state.copyWith(
|
String deviceAssetId,
|
||||||
selectedAlbumsBackupAssetsIds: {
|
String deviceId,
|
||||||
...state.selectedAlbumsBackupAssetsIds,
|
bool isDuplicated,
|
||||||
deviceAssetId
|
) {
|
||||||
},
|
if (isDuplicated) {
|
||||||
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
|
state = state.copyWith(
|
||||||
);
|
allUniqueAssets: state.allUniqueAssets
|
||||||
|
.where((asset) => asset.id != deviceAssetId)
|
||||||
|
.toSet(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
state = state.copyWith(
|
||||||
|
selectedAlbumsBackupAssetsIds: {
|
||||||
|
...state.selectedAlbumsBackupAssetsIds,
|
||||||
|
deviceAssetId
|
||||||
|
},
|
||||||
|
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.allUniqueAssets.length -
|
if (state.allUniqueAssets.length -
|
||||||
state.selectedAlbumsBackupAssetsIds.length ==
|
state.selectedAlbumsBackupAssetsIds.length ==
|
||||||
@@ -564,6 +584,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
albums.lastExcludedBackupTime,
|
albums.lastExcludedBackupTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
|
||||||
final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
|
final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: previous,
|
backupProgress: previous,
|
||||||
@@ -608,6 +629,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
if (Hive.isBoxOpen(duplicatedAssetsBox)) {
|
||||||
|
await Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox).close();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
|
if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
|
||||||
await Hive.box(backgroundBackupInfoBox).close();
|
await Hive.box(backgroundBackupInfoBox).close();
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import 'package:http_parser/http_parser.dart';
|
|||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:cancellation_token_http/http.dart' as http;
|
import 'package:cancellation_token_http/http.dart' as http;
|
||||||
|
|
||||||
|
import '../models/hive_duplicated_assets.model.dart';
|
||||||
|
|
||||||
final backupServiceProvider = Provider(
|
final backupServiceProvider = Provider(
|
||||||
(ref) => BackupService(
|
(ref) => BackupService(
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
@@ -41,6 +43,29 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _saveDuplicatedAssetIdToLocalStorage(List<String> deviceAssetIds) {
|
||||||
|
HiveDuplicatedAssets duplicatedAssets =
|
||||||
|
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
|
||||||
|
.get(duplicatedAssetsKey) ??
|
||||||
|
HiveDuplicatedAssets(duplicatedAssetIds: []);
|
||||||
|
|
||||||
|
duplicatedAssets.duplicatedAssetIds =
|
||||||
|
{...duplicatedAssets.duplicatedAssetIds, ...deviceAssetIds}.toList();
|
||||||
|
|
||||||
|
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
|
||||||
|
.put(duplicatedAssetsKey, duplicatedAssets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get duplicated asset id from Hive storage
|
||||||
|
Set<String> getDuplicatedAssetIds() {
|
||||||
|
HiveDuplicatedAssets duplicatedAssets =
|
||||||
|
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
|
||||||
|
.get(duplicatedAssetsKey) ??
|
||||||
|
HiveDuplicatedAssets(duplicatedAssetIds: []);
|
||||||
|
|
||||||
|
return duplicatedAssets.duplicatedAssetIds.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns all assets newer than the last successful backup per album
|
/// Returns all assets newer than the last successful backup per album
|
||||||
Future<List<AssetEntity>> buildUploadCandidates(
|
Future<List<AssetEntity>> buildUploadCandidates(
|
||||||
HiveBackupAlbums backupAlbums,
|
HiveBackupAlbums backupAlbums,
|
||||||
@@ -140,34 +165,47 @@ class BackupService {
|
|||||||
Future<List<AssetEntity>> removeAlreadyUploadedAssets(
|
Future<List<AssetEntity>> removeAlreadyUploadedAssets(
|
||||||
List<AssetEntity> candidates,
|
List<AssetEntity> candidates,
|
||||||
) async {
|
) async {
|
||||||
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
if (candidates.isEmpty) {
|
||||||
if (candidates.length < 10) {
|
return candidates;
|
||||||
final List<CheckDuplicateAssetResponseDto?> duplicateResponse =
|
}
|
||||||
await Future.wait(
|
final Set<String> duplicatedAssetIds = getDuplicatedAssetIds();
|
||||||
candidates.map(
|
candidates = duplicatedAssetIds.isEmpty
|
||||||
(e) => _apiService.assetApi.checkDuplicateAsset(
|
? candidates
|
||||||
CheckDuplicateAssetDto(deviceAssetId: e.id, deviceId: deviceId),
|
: candidates
|
||||||
),
|
.whereNot((asset) => duplicatedAssetIds.contains(asset.id))
|
||||||
|
.toList();
|
||||||
|
if (candidates.isEmpty) {
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
final Set<String> existing = {};
|
||||||
|
try {
|
||||||
|
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
final CheckExistingAssetsResponseDto? duplicates =
|
||||||
|
await _apiService.assetApi.checkExistingAssets(
|
||||||
|
CheckExistingAssetsDto(
|
||||||
|
deviceAssetIds: candidates.map((e) => e.id).toList(),
|
||||||
|
deviceId: deviceId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return candidates
|
if (duplicates != null) {
|
||||||
.whereIndexed((i, e) => duplicateResponse[i]?.isExist == false)
|
existing.addAll(duplicates.existingIds);
|
||||||
.toList();
|
}
|
||||||
} else {
|
} on ApiException {
|
||||||
final List<String>? allAssetsInDatabase = await getDeviceBackupAsset();
|
// workaround for older server versions or when checking for too many assets at once
|
||||||
|
final List<String>? allAssetsInDatabase = await getDeviceBackupAsset();
|
||||||
if (allAssetsInDatabase == null) {
|
if (allAssetsInDatabase != null) {
|
||||||
return candidates;
|
existing.addAll(allAssetsInDatabase);
|
||||||
}
|
}
|
||||||
final Set<String> inDb = allAssetsInDatabase.toSet();
|
|
||||||
return candidates.whereNot((e) => inDb.contains(e.id)).toList();
|
|
||||||
}
|
}
|
||||||
|
return existing.isEmpty
|
||||||
|
? candidates
|
||||||
|
: candidates.whereNot((e) => existing.contains(e.id)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> backupAsset(
|
Future<bool> backupAsset(
|
||||||
Iterable<AssetEntity> assetList,
|
Iterable<AssetEntity> assetList,
|
||||||
http.CancellationToken cancelToken,
|
http.CancellationToken cancelToken,
|
||||||
Function(String, String) singleAssetDoneCb,
|
Function(String, String, bool) uploadSuccessCb,
|
||||||
Function(int, int) uploadProgressCb,
|
Function(int, int) uploadProgressCb,
|
||||||
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
|
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
|
||||||
Function(ErrorUploadAsset) errorCb,
|
Function(ErrorUploadAsset) errorCb,
|
||||||
@@ -176,6 +214,7 @@ class BackupService {
|
|||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
File? file;
|
File? file;
|
||||||
bool anyErrors = false;
|
bool anyErrors = false;
|
||||||
|
final List<String> duplicatedAssetIds = [];
|
||||||
|
|
||||||
for (var entity in assetList) {
|
for (var entity in assetList) {
|
||||||
try {
|
try {
|
||||||
@@ -235,8 +274,13 @@ class BackupService {
|
|||||||
|
|
||||||
var response = await req.send(cancellationToken: cancelToken);
|
var response = await req.send(cancellationToken: cancelToken);
|
||||||
|
|
||||||
if (response.statusCode == 201) {
|
if (response.statusCode == 200) {
|
||||||
singleAssetDoneCb(entity.id, deviceId);
|
// asset is a duplicate (already exists on the server)
|
||||||
|
duplicatedAssetIds.add(entity.id);
|
||||||
|
uploadSuccessCb(entity.id, deviceId, true);
|
||||||
|
} else if (response.statusCode == 201) {
|
||||||
|
// stored a new asset on the server
|
||||||
|
uploadSuccessCb(entity.id, deviceId, false);
|
||||||
} else {
|
} else {
|
||||||
var data = await response.stream.bytesToString();
|
var data = await response.stream.bytesToString();
|
||||||
var error = jsonDecode(data);
|
var error = jsonDecode(data);
|
||||||
@@ -260,7 +304,8 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
} on http.CancelledException {
|
} on http.CancelledException {
|
||||||
debugPrint("Backup was cancelled by the user");
|
debugPrint("Backup was cancelled by the user");
|
||||||
return false;
|
anyErrors = true;
|
||||||
|
break;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("ERROR backupAsset: ${e.toString()}");
|
debugPrint("ERROR backupAsset: ${e.toString()}");
|
||||||
anyErrors = true;
|
anyErrors = true;
|
||||||
@@ -271,6 +316,9 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (duplicatedAssetIds.isNotEmpty) {
|
||||||
|
_saveDuplicatedAssetIdToLocalStorage(duplicatedAssetIds);
|
||||||
|
}
|
||||||
return !anyErrors;
|
return !anyErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
final assets = useState<List<AssetEntity>>([]);
|
final assets = useState<List<AssetEntity>>([]);
|
||||||
|
|
||||||
_getAssetsInAlbum() async {
|
_getAssetsInAlbum() async {
|
||||||
assets.value =
|
assets.value = await album.getAssetListRange(
|
||||||
await album.getAssetListRange(start: 0, end: album.assetCount);
|
start: 0, end: await album.assetCountAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -34,7 +34,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
title: Column(
|
title: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${album.name} (${album.assetCount})",
|
"${album.name} (${album.assetCountAsync})",
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showBatteryOptimizationInfoToUser() {
|
void _showBatteryOptimizationInfoToUser() {
|
||||||
final buttonTextColor = Theme.of(context).primaryColor;
|
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
@@ -173,13 +172,14 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
OutlinedButton(
|
ElevatedButton(
|
||||||
onPressed: () => launchUrl(
|
onPressed: () => launchUrl(
|
||||||
Uri.parse('https://dontkillmyapp.com'),
|
Uri.parse('https://dontkillmyapp.com'),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"backup_controller_page_background_battery_info_link",
|
"backup_controller_page_background_battery_info_link",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
@@ -220,7 +220,12 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!isBackgroundEnabled)
|
if (!isBackgroundEnabled)
|
||||||
const Text("backup_controller_page_background_description").tr(),
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child:
|
||||||
|
const Text("backup_controller_page_background_description")
|
||||||
|
.tr(),
|
||||||
|
),
|
||||||
if (isBackgroundEnabled)
|
if (isBackgroundEnabled)
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title:
|
title:
|
||||||
@@ -414,7 +419,6 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
ActionChip(
|
ActionChip(
|
||||||
avatar: Icon(
|
avatar: Icon(
|
||||||
Icons.info,
|
Icons.info,
|
||||||
size: 24,
|
|
||||||
color: Colors.red[400],
|
color: Colors.red[400],
|
||||||
),
|
),
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class HomePageState {
|
|
||||||
final bool isMultiSelectEnable;
|
|
||||||
final Set<AssetResponseDto> selectedItems;
|
|
||||||
final Set<String> selectedDateGroup;
|
|
||||||
HomePageState({
|
|
||||||
required this.isMultiSelectEnable,
|
|
||||||
required this.selectedItems,
|
|
||||||
required this.selectedDateGroup,
|
|
||||||
});
|
|
||||||
|
|
||||||
HomePageState copyWith({
|
|
||||||
bool? isMultiSelectEnable,
|
|
||||||
Set<AssetResponseDto>? selectedItems,
|
|
||||||
Set<String>? selectedDateGroup,
|
|
||||||
}) {
|
|
||||||
return HomePageState(
|
|
||||||
isMultiSelectEnable: isMultiSelectEnable ?? this.isMultiSelectEnable,
|
|
||||||
selectedItems: selectedItems ?? this.selectedItems,
|
|
||||||
selectedDateGroup: selectedDateGroup ?? this.selectedDateGroup,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() =>
|
|
||||||
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
final setEquals = const DeepCollectionEquality().equals;
|
|
||||||
|
|
||||||
return other is HomePageState &&
|
|
||||||
other.isMultiSelectEnable == isMultiSelectEnable &&
|
|
||||||
setEquals(other.selectedItems, selectedItems) &&
|
|
||||||
setEquals(other.selectedDateGroup, selectedDateGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
isMultiSelectEnable.hashCode ^
|
|
||||||
selectedItems.hashCode ^
|
|
||||||
selectedDateGroup.hashCode;
|
|
||||||
}
|
|
||||||
@@ -1,95 +1,14 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
enum RenderAssetGridElementType {
|
|
||||||
assetRow,
|
|
||||||
dayTitle,
|
|
||||||
monthTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RenderAssetGridRow {
|
|
||||||
final List<AssetResponseDto> assets;
|
|
||||||
|
|
||||||
RenderAssetGridRow(this.assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RenderAssetGridElement {
|
|
||||||
final RenderAssetGridElementType type;
|
|
||||||
final RenderAssetGridRow? assetRow;
|
|
||||||
final String? title;
|
|
||||||
final DateTime date;
|
|
||||||
final List<AssetResponseDto>? relatedAssetList;
|
|
||||||
|
|
||||||
RenderAssetGridElement(
|
|
||||||
this.type, {
|
|
||||||
this.assetRow,
|
|
||||||
this.title,
|
|
||||||
required this.date,
|
|
||||||
this.relatedAssetList,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final renderListProvider = StateProvider((ref) {
|
final renderListProvider = StateProvider((ref) {
|
||||||
var assetGroups = ref.watch(assetGroupByDateTimeProvider);
|
var assetGroups = ref.watch(assetGroupByDateTimeProvider);
|
||||||
var settings = ref.watch(appSettingsServiceProvider);
|
|
||||||
|
|
||||||
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
||||||
|
|
||||||
List<RenderAssetGridElement> elements = [];
|
return assetGroupsToRenderList(assetGroups, assetsPerRow);
|
||||||
DateTime? lastDate;
|
|
||||||
|
|
||||||
assetGroups.forEach((groupName, assets) {
|
|
||||||
try {
|
|
||||||
final date = DateTime.parse(groupName);
|
|
||||||
|
|
||||||
if (lastDate == null || lastDate!.month != date.month) {
|
|
||||||
elements.add(
|
|
||||||
RenderAssetGridElement(
|
|
||||||
RenderAssetGridElementType.monthTitle,
|
|
||||||
title: groupName,
|
|
||||||
date: date,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add group title
|
|
||||||
elements.add(
|
|
||||||
RenderAssetGridElement(
|
|
||||||
RenderAssetGridElementType.dayTitle,
|
|
||||||
title: groupName,
|
|
||||||
date: date,
|
|
||||||
relatedAssetList: assets,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add rows
|
|
||||||
int cursor = 0;
|
|
||||||
while (cursor < assets.length) {
|
|
||||||
int rowElements = min(assets.length - cursor, assetsPerRow);
|
|
||||||
|
|
||||||
final rowElement = RenderAssetGridElement(
|
|
||||||
RenderAssetGridElementType.assetRow,
|
|
||||||
date: date,
|
|
||||||
assetRow: RenderAssetGridRow(
|
|
||||||
assets.sublist(cursor, cursor + rowElements),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
elements.add(rowElement);
|
|
||||||
cursor += rowElements;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastDate = date;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint(e.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/share.service.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
|
||||||
|
|
||||||
final ShareService _shareService;
|
|
||||||
|
|
||||||
HomePageStateNotifier(this._shareService)
|
|
||||||
: super(
|
|
||||||
HomePageState(
|
|
||||||
isMultiSelectEnable: false,
|
|
||||||
selectedItems: {},
|
|
||||||
selectedDateGroup: {},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
void addSelectedDateGroup(String dateGroupTitle) {
|
|
||||||
state = state.copyWith(
|
|
||||||
selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeSelectedDateGroup(String dateGroupTitle) {
|
|
||||||
var currentDateGroup = state.selectedDateGroup;
|
|
||||||
|
|
||||||
currentDateGroup.removeWhere((e) => e == dateGroupTitle);
|
|
||||||
|
|
||||||
state = state.copyWith(selectedDateGroup: currentDateGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
void enableMultiSelect(Set<AssetResponseDto> selectedItems) {
|
|
||||||
state =
|
|
||||||
state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
void disableMultiSelect() {
|
|
||||||
state = state.copyWith(
|
|
||||||
isMultiSelectEnable: false,
|
|
||||||
selectedItems: {},
|
|
||||||
selectedDateGroup: {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addSingleSelectedItem(AssetResponseDto asset) {
|
|
||||||
state = state.copyWith(selectedItems: {...state.selectedItems, asset});
|
|
||||||
}
|
|
||||||
|
|
||||||
void addMultipleSelectedItems(List<AssetResponseDto> assets) {
|
|
||||||
state = state.copyWith(selectedItems: {...state.selectedItems, ...assets});
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeSingleSelectedItem(AssetResponseDto asset) {
|
|
||||||
Set<AssetResponseDto> currentList = state.selectedItems;
|
|
||||||
|
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
|
||||||
|
|
||||||
state = state.copyWith(selectedItems: currentList);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeMultipleSelectedItem(List<AssetResponseDto> assets) {
|
|
||||||
Set<AssetResponseDto> currentList = state.selectedItems;
|
|
||||||
|
|
||||||
for (AssetResponseDto asset in assets) {
|
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(selectedItems: currentList);
|
|
||||||
}
|
|
||||||
|
|
||||||
void shareAssets(List<AssetResponseDto> assets, BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext buildContext) {
|
|
||||||
_shareService
|
|
||||||
.shareAssets(assets)
|
|
||||||
.then((_) => Navigator.of(buildContext).pop());
|
|
||||||
return const ShareDialog();
|
|
||||||
},
|
|
||||||
barrierDismissible: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final homePageStateProvider =
|
|
||||||
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
|
|
||||||
((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))),
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
final multiselectProvider = StateProvider((ref) {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
37
mobile/lib/modules/home/services/asset_cache.service.dart
Normal file
37
mobile/lib/modules/home/services/asset_cache.service.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/json_cache.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
|
||||||
|
AssetCacheService() : super("asset_cache");
|
||||||
|
|
||||||
|
@override
|
||||||
|
void put(List<AssetResponseDto> data) {
|
||||||
|
putRawData(data.map((e) => e.toJson()).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<AssetResponseDto>> get() async {
|
||||||
|
try {
|
||||||
|
final mapList = await readRawData() as List<dynamic>;
|
||||||
|
|
||||||
|
final responseData = mapList
|
||||||
|
.map((e) => AssetResponseDto.fromJson(e))
|
||||||
|
.whereNotNull()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(e.toString());
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCacheServiceProvider = Provider(
|
||||||
|
(ref) => AssetCacheService(),
|
||||||
|
);
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
enum RenderAssetGridElementType {
|
||||||
|
assetRow,
|
||||||
|
dayTitle,
|
||||||
|
monthTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenderAssetGridRow {
|
||||||
|
final List<AssetResponseDto> assets;
|
||||||
|
|
||||||
|
RenderAssetGridRow(this.assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenderAssetGridElement {
|
||||||
|
final RenderAssetGridElementType type;
|
||||||
|
final RenderAssetGridRow? assetRow;
|
||||||
|
final String? title;
|
||||||
|
final DateTime date;
|
||||||
|
final List<AssetResponseDto>? relatedAssetList;
|
||||||
|
|
||||||
|
RenderAssetGridElement(
|
||||||
|
this.type, {
|
||||||
|
this.assetRow,
|
||||||
|
this.title,
|
||||||
|
required this.date,
|
||||||
|
this.relatedAssetList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RenderAssetGridElement> assetsToRenderList(
|
||||||
|
List<AssetResponseDto> assets, int assetsPerRow) {
|
||||||
|
List<RenderAssetGridElement> elements = [];
|
||||||
|
|
||||||
|
int cursor = 0;
|
||||||
|
while (cursor < assets.length) {
|
||||||
|
int rowElements = min(assets.length - cursor, assetsPerRow);
|
||||||
|
final date = DateTime.parse(assets[cursor].createdAt);
|
||||||
|
|
||||||
|
final rowElement = RenderAssetGridElement(
|
||||||
|
RenderAssetGridElementType.assetRow,
|
||||||
|
date: date,
|
||||||
|
assetRow: RenderAssetGridRow(
|
||||||
|
assets.sublist(cursor, cursor + rowElements),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.add(rowElement);
|
||||||
|
cursor += rowElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RenderAssetGridElement> assetGroupsToRenderList(
|
||||||
|
Map<String, List<AssetResponseDto>> assetGroups, int assetsPerRow) {
|
||||||
|
List<RenderAssetGridElement> elements = [];
|
||||||
|
DateTime? lastDate;
|
||||||
|
|
||||||
|
assetGroups.forEach((groupName, assets) {
|
||||||
|
final date = DateTime.parse(groupName);
|
||||||
|
|
||||||
|
if (lastDate == null || lastDate!.month != date.month) {
|
||||||
|
elements.add(
|
||||||
|
RenderAssetGridElement(RenderAssetGridElementType.monthTitle,
|
||||||
|
title: groupName, date: date),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add group title
|
||||||
|
elements.add(
|
||||||
|
RenderAssetGridElement(
|
||||||
|
RenderAssetGridElementType.dayTitle,
|
||||||
|
title: groupName,
|
||||||
|
date: date,
|
||||||
|
relatedAssetList: assets,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add rows
|
||||||
|
int cursor = 0;
|
||||||
|
while (cursor < assets.length) {
|
||||||
|
int rowElements = min(assets.length - cursor, assetsPerRow);
|
||||||
|
|
||||||
|
final rowElement = RenderAssetGridElement(
|
||||||
|
RenderAssetGridElementType.assetRow,
|
||||||
|
date: date,
|
||||||
|
assetRow: RenderAssetGridRow(
|
||||||
|
assets.sublist(cursor, cursor + rowElements),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.add(rowElement);
|
||||||
|
cursor += rowElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDate = date;
|
||||||
|
});
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
72
mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart
Normal file
72
mobile/lib/modules/home/ui/asset_grid/daily_title_text.dart
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
class DailyTitleText extends ConsumerWidget {
|
||||||
|
const DailyTitleText({
|
||||||
|
Key? key,
|
||||||
|
required this.isoDate,
|
||||||
|
required this.multiselectEnabled,
|
||||||
|
required this.onSelect,
|
||||||
|
required this.onDeselect,
|
||||||
|
required this.selected,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String isoDate;
|
||||||
|
final bool multiselectEnabled;
|
||||||
|
final Function onSelect;
|
||||||
|
final Function onDeselect;
|
||||||
|
final bool selected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
var currentYear = DateTime.now().year;
|
||||||
|
var groupYear = DateTime.parse(isoDate).year;
|
||||||
|
var formatDateTemplate = currentYear == groupYear
|
||||||
|
? "daily_title_text_date".tr()
|
||||||
|
: "daily_title_text_date_year".tr();
|
||||||
|
var dateText =
|
||||||
|
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
||||||
|
|
||||||
|
void handleTitleIconClick() {
|
||||||
|
if (selected) {
|
||||||
|
onDeselect();
|
||||||
|
} else {
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 29.0,
|
||||||
|
bottom: 29.0,
|
||||||
|
left: 12.0,
|
||||||
|
right: 12.0,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
dateText,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: handleTitleIconClick,
|
||||||
|
child: multiselectEnabled && selected
|
||||||
|
? Icon(
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
)
|
||||||
|
: const Icon(
|
||||||
|
Icons.check_circle_outline_rounded,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,11 +13,8 @@ class DisableMultiSelectButton extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Positioned(
|
return Padding(
|
||||||
top: 10,
|
padding: const EdgeInsets.only(left: 16.0, top: 15),
|
||||||
left: 0,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 46),
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
@@ -34,7 +31,6 @@ class DisableMultiSelectButton extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
274
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
Normal file
274
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
import 'asset_grid_data_structure.dart';
|
||||||
|
import 'daily_title_text.dart';
|
||||||
|
import 'disable_multi_select_button.dart';
|
||||||
|
import 'draggable_scrollbar_custom.dart';
|
||||||
|
|
||||||
|
typedef ImmichAssetGridSelectionListener = void Function(
|
||||||
|
bool,
|
||||||
|
Set<AssetResponseDto>,
|
||||||
|
);
|
||||||
|
|
||||||
|
class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
|
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||||
|
final ItemPositionsListener _itemPositionsListener =
|
||||||
|
ItemPositionsListener.create();
|
||||||
|
|
||||||
|
bool _scrolling = false;
|
||||||
|
final Set<String> _selectedAssets = HashSet();
|
||||||
|
|
||||||
|
List<AssetResponseDto> get _assets {
|
||||||
|
return widget.renderList
|
||||||
|
.map((e) {
|
||||||
|
if (e.type == RenderAssetGridElementType.assetRow) {
|
||||||
|
return e.assetRow!.assets;
|
||||||
|
} else {
|
||||||
|
return List<AssetResponseDto>.empty();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flattened
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<AssetResponseDto> _getSelectedAssets() {
|
||||||
|
return _selectedAssets
|
||||||
|
.map((e) => _assets.firstWhereOrNull((a) => a.id == e))
|
||||||
|
.whereNotNull()
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _callSelectionListener(bool selectionActive) {
|
||||||
|
widget.listener?.call(selectionActive, _getSelectedAssets());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectAssets(List<AssetResponseDto> assets) {
|
||||||
|
setState(() {
|
||||||
|
for (var e in assets) {
|
||||||
|
_selectedAssets.add(e.id);
|
||||||
|
}
|
||||||
|
_callSelectionListener(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deselectAssets(List<AssetResponseDto> assets) {
|
||||||
|
setState(() {
|
||||||
|
for (var e in assets) {
|
||||||
|
_selectedAssets.remove(e.id);
|
||||||
|
}
|
||||||
|
_callSelectionListener(_selectedAssets.isNotEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deselectAll() {
|
||||||
|
setState(() {
|
||||||
|
_selectedAssets.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
_callSelectionListener(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _allAssetsSelected(List<AssetResponseDto> assets) {
|
||||||
|
return widget.selectionActive &&
|
||||||
|
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getItemSize(BuildContext context) {
|
||||||
|
return MediaQuery.of(context).size.width / widget.assetsPerRow -
|
||||||
|
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildThumbnailOrPlaceholder(
|
||||||
|
AssetResponseDto asset,
|
||||||
|
bool placeholder,
|
||||||
|
) {
|
||||||
|
if (placeholder) {
|
||||||
|
return const DecoratedBox(
|
||||||
|
decoration: BoxDecoration(color: Colors.grey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ThumbnailImage(
|
||||||
|
asset: asset,
|
||||||
|
assetList: _assets,
|
||||||
|
multiselectEnabled: widget.selectionActive,
|
||||||
|
isSelected: _selectedAssets.contains(asset.id),
|
||||||
|
onSelect: () => _selectAssets([asset]),
|
||||||
|
onDeselect: () => _deselectAssets([asset]),
|
||||||
|
useGrayBoxPlaceholder: true,
|
||||||
|
showStorageIndicator: widget.showStorageIndicator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAssetRow(
|
||||||
|
BuildContext context,
|
||||||
|
RenderAssetGridRow row,
|
||||||
|
bool scrolling,
|
||||||
|
) {
|
||||||
|
double size = _getItemSize(context);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
key: Key("asset-row-${row.assets.first.id}"),
|
||||||
|
children: row.assets.map((AssetResponseDto asset) {
|
||||||
|
bool last = asset == row.assets.last;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
key: Key("asset-${asset.id}"),
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: widget.margin,
|
||||||
|
right: last ? 0.0 : widget.margin,
|
||||||
|
),
|
||||||
|
child: _buildThumbnailOrPlaceholder(asset, scrolling),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitle(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
List<AssetResponseDto> assets,
|
||||||
|
) {
|
||||||
|
return DailyTitleText(
|
||||||
|
isoDate: title,
|
||||||
|
multiselectEnabled: widget.selectionActive,
|
||||||
|
onSelect: () => _selectAssets(assets),
|
||||||
|
onDeselect: () => _deselectAssets(assets),
|
||||||
|
selected: _allAssetsSelected(assets),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMonthTitle(BuildContext context, String title) {
|
||||||
|
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
|
||||||
|
.format(DateTime.parse(title));
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
key: Key("month-$title"),
|
||||||
|
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
||||||
|
child: Text(
|
||||||
|
monthTitleText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).textTheme.headline1?.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _itemBuilder(BuildContext c, int position) {
|
||||||
|
final item = widget.renderList[position];
|
||||||
|
|
||||||
|
if (item.type == RenderAssetGridElementType.dayTitle) {
|
||||||
|
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
||||||
|
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
||||||
|
return _buildMonthTitle(c, item.title!);
|
||||||
|
} else if (item.type == RenderAssetGridElementType.assetRow) {
|
||||||
|
return _buildAssetRow(c, item.assetRow!, _scrolling);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Text("Invalid widget type!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Text _labelBuilder(int pos) {
|
||||||
|
final date = widget.renderList[pos].date;
|
||||||
|
return Text(
|
||||||
|
DateFormat.yMMMd().format(date),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMultiSelectIndicator() {
|
||||||
|
return DisableMultiSelectButton(
|
||||||
|
onPressed: () => _deselectAll(),
|
||||||
|
selectedItemCount: _selectedAssets.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAssetGrid() {
|
||||||
|
final useDragScrolling = _assets.length >= 20;
|
||||||
|
|
||||||
|
void dragScrolling(bool active) {
|
||||||
|
setState(() {
|
||||||
|
_scrolling = active;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final listWidget = ScrollablePositionedList.builder(
|
||||||
|
itemBuilder: _itemBuilder,
|
||||||
|
itemPositionsListener: _itemPositionsListener,
|
||||||
|
itemScrollController: _itemScrollController,
|
||||||
|
itemCount: widget.renderList.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!useDragScrolling) {
|
||||||
|
return listWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DraggableScrollbar.semicircle(
|
||||||
|
scrollStateListener: dragScrolling,
|
||||||
|
itemPositionsListener: _itemPositionsListener,
|
||||||
|
controller: _itemScrollController,
|
||||||
|
backgroundColor: Theme.of(context).hintColor,
|
||||||
|
labelTextBuilder: _labelBuilder,
|
||||||
|
labelConstraints: const BoxConstraints(maxHeight: 28),
|
||||||
|
scrollbarAnimationDuration: const Duration(seconds: 1),
|
||||||
|
scrollbarTimeToFade: const Duration(seconds: 4),
|
||||||
|
child: listWidget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ImmichAssetGrid oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (!widget.selectionActive) {
|
||||||
|
setState(() {
|
||||||
|
_selectedAssets.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
_buildAssetGrid(),
|
||||||
|
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImmichAssetGrid extends StatefulWidget {
|
||||||
|
final List<RenderAssetGridElement> renderList;
|
||||||
|
final int assetsPerRow;
|
||||||
|
final double margin;
|
||||||
|
final bool showStorageIndicator;
|
||||||
|
final ImmichAssetGridSelectionListener? listener;
|
||||||
|
final bool selectionActive;
|
||||||
|
|
||||||
|
const ImmichAssetGrid({
|
||||||
|
super.key,
|
||||||
|
required this.renderList,
|
||||||
|
required this.assetsPerRow,
|
||||||
|
required this.showStorageIndicator,
|
||||||
|
this.listener,
|
||||||
|
this.margin = 5.0,
|
||||||
|
this.selectionActive = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return ImmichAssetGridState();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
@@ -16,6 +15,10 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
final List<AssetResponseDto> assetList;
|
final List<AssetResponseDto> assetList;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final bool useGrayBoxPlaceholder;
|
final bool useGrayBoxPlaceholder;
|
||||||
|
final bool isSelected;
|
||||||
|
final bool multiselectEnabled;
|
||||||
|
final Function? onSelect;
|
||||||
|
final Function? onDeselect;
|
||||||
|
|
||||||
const ThumbnailImage({
|
const ThumbnailImage({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -23,19 +26,21 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
required this.assetList,
|
required this.assetList,
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator = true,
|
||||||
this.useGrayBoxPlaceholder = false,
|
this.useGrayBoxPlaceholder = false,
|
||||||
|
this.isSelected = false,
|
||||||
|
this.multiselectEnabled = false,
|
||||||
|
this.onDeselect,
|
||||||
|
this.onSelect,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
||||||
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
|
|
||||||
var isMultiSelectEnable =
|
|
||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
|
|
||||||
|
|
||||||
Widget buildSelectionIcon(AssetResponseDto asset) {
|
Widget buildSelectionIcon(AssetResponseDto asset) {
|
||||||
if (selectedAsset.contains(asset)) {
|
if (isSelected) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
@@ -50,20 +55,12 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (isMultiSelectEnable &&
|
if (multiselectEnabled) {
|
||||||
selectedAsset.contains(asset) &&
|
if (isSelected) {
|
||||||
selectedAsset.length == 1) {
|
onDeselect?.call();
|
||||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
} else {
|
||||||
} else if (isMultiSelectEnable &&
|
onSelect?.call();
|
||||||
selectedAsset.contains(asset) &&
|
}
|
||||||
selectedAsset.length > 1) {
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.removeSingleSelectedItem(asset);
|
|
||||||
} else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.addSingleSelectedItem(asset);
|
|
||||||
} else {
|
} else {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
GalleryViewerRoute(
|
GalleryViewerRoute(
|
||||||
@@ -74,8 +71,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
// Enable multi select function
|
onSelect?.call();
|
||||||
ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
|
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
},
|
},
|
||||||
child: Hero(
|
child: Hero(
|
||||||
@@ -84,7 +80,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: isMultiSelectEnable && selectedAsset.contains(asset)
|
border: multiselectEnabled && isSelected
|
||||||
? Border.all(
|
? Border.all(
|
||||||
color: Theme.of(context).primaryColorLight,
|
color: Theme.of(context).primaryColorLight,
|
||||||
width: 10,
|
width: 10,
|
||||||
@@ -128,7 +124,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isMultiSelectEnable)
|
if (multiselectEnabled)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(3.0),
|
padding: const EdgeInsets.all(3.0),
|
||||||
child: Align(
|
child: Align(
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class DailyTitleText extends ConsumerWidget {
|
|
||||||
const DailyTitleText({
|
|
||||||
Key? key,
|
|
||||||
required this.isoDate,
|
|
||||||
required this.assetGroup,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final String isoDate;
|
|
||||||
final List<AssetResponseDto> assetGroup;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
var currentYear = DateTime.now().year;
|
|
||||||
var groupYear = DateTime.parse(isoDate).year;
|
|
||||||
var formatDateTemplate = currentYear == groupYear
|
|
||||||
? "daily_title_text_date".tr()
|
|
||||||
: "daily_title_text_date_year".tr();
|
|
||||||
var dateText =
|
|
||||||
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
|
||||||
var isMultiSelectEnable =
|
|
||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
|
||||||
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
|
|
||||||
var selectedItems = ref.watch(homePageStateProvider).selectedItems;
|
|
||||||
|
|
||||||
void _handleTitleIconClick() {
|
|
||||||
if (isMultiSelectEnable &&
|
|
||||||
selectedDateGroup.contains(dateText) &&
|
|
||||||
selectedDateGroup.length == 1 &&
|
|
||||||
selectedItems.length <= assetGroup.length) {
|
|
||||||
// Multi select is active - click again on the icon while it is the only active group -> disable multi select
|
|
||||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
|
||||||
} else if (isMultiSelectEnable &&
|
|
||||||
selectedDateGroup.contains(dateText) &&
|
|
||||||
selectedItems.length != assetGroup.length) {
|
|
||||||
// Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.removeSelectedDateGroup(dateText);
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.removeMultipleSelectedItem(assetGroup);
|
|
||||||
} else if (isMultiSelectEnable &&
|
|
||||||
selectedDateGroup.contains(dateText) &&
|
|
||||||
selectedDateGroup.length > 1) {
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.removeSelectedDateGroup(dateText);
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.removeMultipleSelectedItem(assetGroup);
|
|
||||||
} else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.addSelectedDateGroup(dateText);
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.addMultipleSelectedItems(assetGroup);
|
|
||||||
} else {
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.enableMultiSelect(assetGroup.toSet());
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.addSelectedDateGroup(dateText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 29.0,
|
|
||||||
bottom: 29.0,
|
|
||||||
left: 12.0,
|
|
||||||
right: 12.0,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
dateText,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: _handleTitleIconClick,
|
|
||||||
child: isMultiSelectEnable && selectedDateGroup.contains(dateText)
|
|
||||||
? Icon(
|
|
||||||
Icons.check_circle_rounded,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
)
|
|
||||||
: const Icon(
|
|
||||||
Icons.check_circle_outline_rounded,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/src/widgets/framework.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_list_v2/daily_title_text.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
|
||||||
|
|
||||||
import '../thumbnail_image.dart';
|
|
||||||
|
|
||||||
class ImmichAssetGrid extends HookConsumerWidget {
|
|
||||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
|
||||||
final ItemPositionsListener _itemPositionsListener =
|
|
||||||
ItemPositionsListener.create();
|
|
||||||
|
|
||||||
final List<RenderAssetGridElement> renderList;
|
|
||||||
final int assetsPerRow;
|
|
||||||
final double margin;
|
|
||||||
final bool showStorageIndicator;
|
|
||||||
|
|
||||||
ImmichAssetGrid({
|
|
||||||
super.key,
|
|
||||||
required this.renderList,
|
|
||||||
required this.assetsPerRow,
|
|
||||||
required this.showStorageIndicator,
|
|
||||||
this.margin = 5.0,
|
|
||||||
});
|
|
||||||
|
|
||||||
List<AssetResponseDto> get _assets {
|
|
||||||
return renderList
|
|
||||||
.map((e) {
|
|
||||||
if (e.type == RenderAssetGridElementType.assetRow) {
|
|
||||||
return e.assetRow!.assets;
|
|
||||||
} else {
|
|
||||||
return List<AssetResponseDto>.empty();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flattened
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
double _getItemSize(BuildContext context) {
|
|
||||||
return MediaQuery.of(context).size.width / assetsPerRow -
|
|
||||||
margin * (assetsPerRow - 1) / assetsPerRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildThumbnailOrPlaceholder(
|
|
||||||
AssetResponseDto asset, bool placeholder) {
|
|
||||||
if (placeholder) {
|
|
||||||
return const DecoratedBox(
|
|
||||||
decoration: BoxDecoration(color: Colors.grey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ThumbnailImage(
|
|
||||||
asset: asset,
|
|
||||||
assetList: _assets,
|
|
||||||
showStorageIndicator: showStorageIndicator,
|
|
||||||
useGrayBoxPlaceholder: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAssetRow(
|
|
||||||
BuildContext context, RenderAssetGridRow row, bool scrolling) {
|
|
||||||
double size = _getItemSize(context);
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
key: Key("asset-row-${row.assets.first.id}"),
|
|
||||||
children: row.assets.map((AssetResponseDto asset) {
|
|
||||||
bool last = asset == row.assets.last;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
key: Key("asset-${asset.id}"),
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
margin: EdgeInsets.only(top: margin, right: last ? 0.0 : margin),
|
|
||||||
child: _buildThumbnailOrPlaceholder(asset, scrolling),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTitle(
|
|
||||||
BuildContext context, String title, List<AssetResponseDto> assets) {
|
|
||||||
return DailyTitleText(
|
|
||||||
isoDate: title,
|
|
||||||
assetGroup: assets,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMonthTitle(BuildContext context, String title) {
|
|
||||||
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
|
|
||||||
.format(DateTime.parse(title));
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
key: Key("month-$title"),
|
|
||||||
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
|
||||||
child: Text(
|
|
||||||
monthTitleText,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 26,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).textTheme.headline1?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _itemBuilder(BuildContext c, int position, bool scrolling) {
|
|
||||||
final item = renderList[position];
|
|
||||||
|
|
||||||
if (item.type == RenderAssetGridElementType.dayTitle) {
|
|
||||||
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
|
||||||
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
|
||||||
return _buildMonthTitle(c, item.title!);
|
|
||||||
} else if (item.type == RenderAssetGridElementType.assetRow) {
|
|
||||||
return _buildAssetRow(c, item.assetRow!, scrolling);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const Text("Invalid widget type!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Text _labelBuilder(int pos) {
|
|
||||||
final date = renderList[pos].date;
|
|
||||||
return Text(DateFormat.yMMMd().format(date),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final scrolling = useState(false);
|
|
||||||
|
|
||||||
void dragScrolling(bool active) {
|
|
||||||
scrolling.value = active;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget itemBuilder(BuildContext c, int position) {
|
|
||||||
return _itemBuilder(c, position, scrolling.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DraggableScrollbar.semicircle(
|
|
||||||
scrollStateListener: dragScrolling,
|
|
||||||
itemPositionsListener: _itemPositionsListener,
|
|
||||||
controller: _itemScrollController,
|
|
||||||
backgroundColor: Theme.of(context).hintColor,
|
|
||||||
labelTextBuilder: _labelBuilder,
|
|
||||||
labelConstraints: const BoxConstraints(maxHeight: 28),
|
|
||||||
scrollbarAnimationDuration: const Duration(seconds: 1),
|
|
||||||
scrollbarTimeToFade: const Duration(seconds: 4),
|
|
||||||
child: ScrollablePositionedList.builder(
|
|
||||||
itemBuilder: itemBuilder,
|
|
||||||
itemPositionsListener: _itemPositionsListener,
|
|
||||||
itemScrollController: _itemScrollController,
|
|
||||||
itemCount: renderList.length,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
|
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
|
||||||
|
|
||||||
class ControlBottomAppBar extends ConsumerWidget {
|
class ControlBottomAppBar extends ConsumerWidget {
|
||||||
const ControlBottomAppBar({Key? key}) : super(key: key);
|
final Function onShare;
|
||||||
|
final Function onDelete;
|
||||||
|
|
||||||
|
const ControlBottomAppBar(
|
||||||
|
{Key? key, required this.onShare, required this.onDelete})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -36,7 +40,9 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return const DeleteDialog();
|
return DeleteDialog(
|
||||||
|
onDelete: onDelete,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -45,14 +51,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
iconData: Icons.share,
|
iconData: Icons.share,
|
||||||
label: "control_bottom_app_bar_share".tr(),
|
label: "control_bottom_app_bar_share".tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final homePageState = ref.watch(homePageStateProvider);
|
onShare();
|
||||||
ref.watch(homePageStateProvider.notifier).shareAssets(
|
|
||||||
homePageState.selectedItems.toList(),
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.disableMultiSelect();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class DailyTitleText extends ConsumerWidget {
|
|
||||||
const DailyTitleText({
|
|
||||||
Key? key,
|
|
||||||
required this.isoDate,
|
|
||||||
required this.assetGroup,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final String isoDate;
|
|
||||||
final List<AssetResponseDto> assetGroup;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
var currentYear = DateTime.now().year;
|
|
||||||
var groupYear = DateTime.parse(isoDate).year;
|
|
||||||
var formatDateTemplate = currentYear == groupYear
|
|
||||||
? "daily_title_text_date".tr()
|
|
||||||
: "daily_title_text_date_year".tr();
|
|
||||||
var dateText = DateFormat(formatDateTemplate)
|
|
||||||
.format(DateTime.parse(isoDate).toLocal());
|
|
||||||
var isMultiSelectEnable =
|
|
||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
|
||||||
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
|
|
||||||
var selectedItems = ref.watch(homePageStateProvider).selectedItems;
|
|
||||||
|
|
||||||
void _handleTitleIconClick() {
|
|
||||||
if (isMultiSelectEnable &&
|
|
||||||
selectedDateGroup.contains(dateText) &&
|
|
||||||
selectedDateGroup.length == 1 &&
|
|
||||||
selectedItems.length <= assetGroup.length) {
|
|
||||||
// Multi select is active - click again on the icon while it is the only active group -> disable multi select
|
|
||||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
|
||||||
} else if (isMultiSelectEnable &&
|
|
||||||
selectedDateGroup.contains(dateText) &&
|
|
||||||
selectedItems.length != assetGroup.length) {
|
|
||||||
// Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.removeSelectedDateGroup(dateText);
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.removeMultipleSelectedItem(assetGroup);
|
|
||||||
} else if (isMultiSelectEnable &&
|
|
||||||
selectedDateGroup.contains(dateText) &&
|
|
||||||
selectedDateGroup.length > 1) {
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.removeSelectedDateGroup(dateText);
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.removeMultipleSelectedItem(assetGroup);
|
|
||||||
} else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.addSelectedDateGroup(dateText);
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.addMultipleSelectedItems(assetGroup);
|
|
||||||
} else {
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.enableMultiSelect(assetGroup.toSet());
|
|
||||||
ref
|
|
||||||
.watch(homePageStateProvider.notifier)
|
|
||||||
.addSelectedDateGroup(dateText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 29.0,
|
|
||||||
bottom: 29.0,
|
|
||||||
left: 12.0,
|
|
||||||
right: 12.0,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
dateText,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: _handleTitleIconClick,
|
|
||||||
child: isMultiSelectEnable && selectedDateGroup.contains(dateText)
|
|
||||||
? Icon(
|
|
||||||
Icons.check_circle_rounded,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
)
|
|
||||||
: const Icon(
|
|
||||||
Icons.check_circle_outline_rounded,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
|
||||||
|
|
||||||
class DeleteDialog extends ConsumerWidget {
|
class DeleteDialog extends ConsumerWidget {
|
||||||
const DeleteDialog({Key? key}) : super(key: key);
|
final Function onDelete;
|
||||||
|
|
||||||
|
const DeleteDialog({Key? key, required this.onDelete}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final homePageState = ref.watch(homePageStateProvider);
|
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
backgroundColor: Colors.grey[200],
|
// backgroundColor: Colors.grey[200],
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
title: const Text("delete_dialog_title").tr(),
|
title: const Text("delete_dialog_title").tr(),
|
||||||
content: const Text("delete_dialog_alert").tr(),
|
content: const Text("delete_dialog_alert").tr(),
|
||||||
@@ -21,23 +20,25 @@ class DeleteDialog extends ConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text(
|
||||||
"delete_dialog_cancel",
|
"delete_dialog_cancel",
|
||||||
style: TextStyle(color: Colors.blueGrey),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref
|
onDelete();
|
||||||
.watch(assetProvider.notifier)
|
|
||||||
.deleteAssets(homePageState.selectedItems);
|
|
||||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"delete_dialog_ok",
|
"delete_dialog_ok",
|
||||||
style: TextStyle(color: Colors.red[400]),
|
style: TextStyle(
|
||||||
|
color: Colors.red[400],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
|
||||||
class ImageGrid extends ConsumerWidget {
|
|
||||||
final List<AssetResponseDto> assetGroup;
|
|
||||||
final List<AssetResponseDto> sortedAssetGroup;
|
|
||||||
final int tilesPerRow;
|
|
||||||
final bool showStorageIndicator;
|
|
||||||
|
|
||||||
ImageGrid({
|
|
||||||
Key? key,
|
|
||||||
required this.assetGroup,
|
|
||||||
required this.sortedAssetGroup,
|
|
||||||
this.tilesPerRow = 4,
|
|
||||||
this.showStorageIndicator = true,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
List<AssetResponseDto> imageSortedList = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return SliverGrid(
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: tilesPerRow,
|
|
||||||
crossAxisSpacing: 5.0,
|
|
||||||
mainAxisSpacing: 5,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(BuildContext context, int index) {
|
|
||||||
var assetType = assetGroup[index].type;
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {},
|
|
||||||
child: ThumbnailImage(
|
|
||||||
asset: assetGroup[index],
|
|
||||||
assetList: sortedAssetGroup,
|
|
||||||
showStorageIndicator: showStorageIndicator,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: assetGroup.length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,22 +2,17 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
|
import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
|
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class HomePage extends HookConsumerWidget {
|
class HomePage extends HookConsumerWidget {
|
||||||
@@ -26,22 +21,9 @@ class HomePage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
|
|
||||||
var renderList = ref.watch(renderListProvider);
|
var renderList = ref.watch(renderListProvider);
|
||||||
|
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
||||||
ScrollController scrollController = useScrollController();
|
final selection = useState(<AssetResponseDto>{});
|
||||||
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
|
||||||
List<Widget> imageGridGroup = [];
|
|
||||||
var isMultiSelectEnable =
|
|
||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
|
||||||
var homePageState = ref.watch(homePageStateProvider);
|
|
||||||
List<AssetResponseDto> sortedAssetList = [];
|
|
||||||
// set sorted List
|
|
||||||
for (var group in assetGroupByDateTime.values) {
|
|
||||||
for (var value in group) {
|
|
||||||
sortedAssetList.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@@ -57,115 +39,61 @@ class HomePage extends HookConsumerWidget {
|
|||||||
ref.read(assetProvider.notifier).getAllAsset();
|
ref.read(assetProvider.notifier).getAllAsset();
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildSelectedItemCountIndicator() {
|
Widget buildBody() {
|
||||||
return DisableMultiSelectButton(
|
void selectionListener(
|
||||||
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
|
bool multiselect,
|
||||||
selectedItemCount: homePageState.selectedItems.length,
|
Set<AssetResponseDto> selectedAssets,
|
||||||
);
|
) {
|
||||||
}
|
multiselectEnabled.state = multiselect;
|
||||||
|
selection.value = selectedAssets;
|
||||||
Widget _buildBody() {
|
|
||||||
if (assetGroupByDateTime.isNotEmpty) {
|
|
||||||
int? lastMonth;
|
|
||||||
|
|
||||||
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
|
|
||||||
try {
|
|
||||||
DateTime parseDateGroup = DateTime.parse(dateGroup);
|
|
||||||
int currentMonth = parseDateGroup.month;
|
|
||||||
|
|
||||||
if (lastMonth != null) {
|
|
||||||
if (currentMonth - lastMonth! != 0) {
|
|
||||||
imageGridGroup.add(
|
|
||||||
MonthlyTitleText(
|
|
||||||
isoDate: dateGroup,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
imageGridGroup.add(
|
|
||||||
DailyTitleText(
|
|
||||||
key: Key('${dateGroup.toString()}title'),
|
|
||||||
isoDate: dateGroup,
|
|
||||||
assetGroup: immichAssetList,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
imageGridGroup.add(
|
|
||||||
ImageGrid(
|
|
||||||
assetGroup: immichAssetList,
|
|
||||||
sortedAssetGroup: sortedAssetList,
|
|
||||||
tilesPerRow:
|
|
||||||
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
|
||||||
showStorageIndicator: appSettingService
|
|
||||||
.getSetting(AppSettingsEnum.storageIndicator),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
lastMonth = currentMonth;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint(
|
|
||||||
"[ERROR] Cannot parse $dateGroup - Wrong create date format : ${immichAssetList.map((asset) => asset.createdAt).toList()}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildSliverAppBar() {
|
void onShareAssets() {
|
||||||
return isMultiSelectEnable
|
ref.watch(shareServiceProvider).shareAssets(selection.value.toList());
|
||||||
? const SliverToBoxAdapter(
|
multiselectEnabled.state = false;
|
||||||
child: SizedBox(
|
|
||||||
height: 70,
|
|
||||||
child: null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ImmichSliverAppBar(
|
|
||||||
onPopBack: reloadAllAsset,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildAssetGrid() {
|
void onDelete() {
|
||||||
if (appSettingService
|
ref.watch(assetProvider.notifier).deleteAssets(selection.value);
|
||||||
.getSetting(AppSettingsEnum.useExperimentalAssetGrid)) {
|
multiselectEnabled.state = false;
|
||||||
return ImmichAssetGrid(
|
|
||||||
renderList: renderList,
|
|
||||||
assetsPerRow:
|
|
||||||
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
|
||||||
showStorageIndicator: appSettingService
|
|
||||||
.getSetting(AppSettingsEnum.storageIndicator),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return DraggableScrollbar.semicircle(
|
|
||||||
backgroundColor: Theme.of(context).hintColor,
|
|
||||||
controller: scrollController,
|
|
||||||
heightScrollThumb: 48.0,
|
|
||||||
child: CustomScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
slivers: [
|
|
||||||
...imageGridGroup,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: !isMultiSelectEnable,
|
bottom: !multiselectEnabled.state,
|
||||||
top: !isMultiSelectEnable,
|
top: !multiselectEnabled.state,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
CustomScrollView(
|
CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
_buildSliverAppBar(),
|
multiselectEnabled.state
|
||||||
|
? const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 70,
|
||||||
|
child: null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ImmichSliverAppBar(
|
||||||
|
onPopBack: reloadAllAsset,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
|
padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
|
||||||
child: _buildAssetGrid(),
|
child: ImmichAssetGrid(
|
||||||
|
renderList: renderList,
|
||||||
|
assetsPerRow:
|
||||||
|
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
|
showStorageIndicator: appSettingService
|
||||||
|
.getSetting(AppSettingsEnum.storageIndicator),
|
||||||
|
listener: selectionListener,
|
||||||
|
selectionActive: multiselectEnabled.state,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isMultiSelectEnable) ...[
|
if (multiselectEnabled.state) ...[
|
||||||
_buildSelectedItemCountIndicator(),
|
ControlBottomAppBar(
|
||||||
const ControlBottomAppBar(),
|
onShare: onShareAssets,
|
||||||
|
onDelete: onDelete,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -174,7 +102,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const ProfileDrawer(),
|
drawer: const ProfileDrawer(),
|
||||||
body: _buildBody(),
|
body: buildBody(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
@@ -15,6 +18,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
this._deviceInfoService,
|
this._deviceInfoService,
|
||||||
this._backupService,
|
this._backupService,
|
||||||
this._apiService,
|
this._apiService,
|
||||||
|
this._assetCacheService,
|
||||||
|
this._albumCacheService,
|
||||||
|
this._sharedAlbumCacheService,
|
||||||
) : super(
|
) : super(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
@@ -41,6 +47,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
final DeviceInfoService _deviceInfoService;
|
final DeviceInfoService _deviceInfoService;
|
||||||
final BackupService _backupService;
|
final BackupService _backupService;
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
|
final AssetCacheService _assetCacheService;
|
||||||
|
final AlbumCacheService _albumCacheService;
|
||||||
|
final SharedAlbumCacheService _sharedAlbumCacheService;
|
||||||
|
|
||||||
Future<bool> login(
|
Future<bool> login(
|
||||||
String email,
|
String email,
|
||||||
@@ -120,6 +129,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
.delete(savedLoginInfoKey);
|
.delete(savedLoginInfoKey);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
HapticFeedback.vibrate();
|
||||||
debugPrint("Error logging in $e");
|
debugPrint("Error logging in $e");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -151,7 +161,23 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
Future<bool> logout() async {
|
Future<bool> logout() async {
|
||||||
Hive.box(userInfoBox).delete(accessTokenKey);
|
Hive.box(userInfoBox).delete(accessTokenKey);
|
||||||
state = state.copyWith(isAuthenticated: false);
|
state = state.copyWith(isAuthenticated: false);
|
||||||
|
_assetCacheService.invalidate();
|
||||||
|
_albumCacheService.invalidate();
|
||||||
|
_sharedAlbumCacheService.invalidate();
|
||||||
|
|
||||||
|
// Remove login info from local storage
|
||||||
|
var loginInfo =
|
||||||
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
|
||||||
|
if (loginInfo != null) {
|
||||||
|
loginInfo.email = "";
|
||||||
|
loginInfo.password = "";
|
||||||
|
loginInfo.isSaveLogin = false;
|
||||||
|
|
||||||
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
|
||||||
|
savedLoginInfoKey,
|
||||||
|
loginInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,5 +223,8 @@ final authenticationProvider =
|
|||||||
ref.watch(deviceInfoServiceProvider),
|
ref.watch(deviceInfoServiceProvider),
|
||||||
ref.watch(backupServiceProvider),
|
ref.watch(backupServiceProvider),
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
|
ref.watch(assetCacheServiceProvider),
|
||||||
|
ref.watch(albumCacheServiceProvider),
|
||||||
|
ref.watch(sharedAlbumCacheServiceProvider),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ class ChangePasswordButton extends ConsumerWidget {
|
|||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
primary: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
onPrimary: Colors.grey[50],
|
foregroundColor: Colors.grey[50],
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -203,8 +203,8 @@ class LoginButton extends ConsumerWidget {
|
|||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
primary: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
onPrimary: Colors.grey[50],
|
foregroundColor: Colors.grey[50],
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||||
),
|
),
|
||||||
@@ -228,7 +228,7 @@ class LoginButton extends ConsumerWidget {
|
|||||||
AutoRouter.of(context).push(const ChangePasswordRoute());
|
AutoRouter.of(context).push(const ChangePasswordRoute());
|
||||||
} else {
|
} else {
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
AutoRouter.of(context).replace(const TabControllerRoute());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -66,3 +69,12 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
|||||||
.format(DateTime.parse(element.createdAt).toLocal()),
|
.format(DateTime.parse(element.createdAt).toLocal()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final searchRenderListProvider = StateProvider((ref) {
|
||||||
|
var assetGroups = ref.watch(searchResultGroupByDateTimeProvider);
|
||||||
|
|
||||||
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
||||||
|
|
||||||
|
return assetGroupsToRenderList(assetGroups, assetsPerRow);
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
|
||||||
class SearchResultPage extends HookConsumerWidget {
|
class SearchResultPage extends HookConsumerWidget {
|
||||||
const SearchResultPage({Key? key, required this.searchTerm})
|
const SearchResultPage({Key? key, required this.searchTerm})
|
||||||
@@ -21,17 +19,12 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
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);
|
||||||
|
|
||||||
final List<Widget> imageGridGroup = [];
|
|
||||||
|
|
||||||
FocusNode? searchFocusNode;
|
FocusNode? searchFocusNode;
|
||||||
|
|
||||||
List<AssetResponseDto> sortedAssetList = [];
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
searchFocusNode = FocusNode();
|
searchFocusNode = FocusNode();
|
||||||
@@ -117,7 +110,12 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
_buildSearchResult() {
|
_buildSearchResult() {
|
||||||
var searchResultPageState = ref.watch(searchResultPageProvider);
|
var searchResultPageState = ref.watch(searchResultPageProvider);
|
||||||
var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider);
|
var searchResultRenderList = ref.watch(searchRenderListProvider);
|
||||||
|
|
||||||
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
||||||
|
final showStorageIndicator =
|
||||||
|
settings.getSetting(AppSettingsEnum.storageIndicator);
|
||||||
|
|
||||||
if (searchResultPageState.isError) {
|
if (searchResultPageState.isError) {
|
||||||
return const Text("Error");
|
return const Text("Error");
|
||||||
@@ -132,57 +130,11 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchResultPageState.isSuccess) {
|
if (searchResultPageState.isSuccess) {
|
||||||
if (searchResultPageState.searchResult.isNotEmpty) {
|
return ImmichAssetGrid(
|
||||||
int? lastMonth;
|
renderList: searchResultRenderList,
|
||||||
// set sorted List
|
assetsPerRow: assetsPerRow,
|
||||||
for (var group in assetGroupByDateTime.values) {
|
showStorageIndicator: showStorageIndicator,
|
||||||
for (var value in group) {
|
);
|
||||||
sortedAssetList.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
|
|
||||||
DateTime parseDateGroup = DateTime.parse(dateGroup);
|
|
||||||
int currentMonth = parseDateGroup.month;
|
|
||||||
|
|
||||||
if (lastMonth != null) {
|
|
||||||
if (currentMonth - lastMonth! != 0) {
|
|
||||||
imageGridGroup.add(
|
|
||||||
MonthlyTitleText(
|
|
||||||
isoDate: dateGroup,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
imageGridGroup.add(
|
|
||||||
DailyTitleText(
|
|
||||||
isoDate: dateGroup,
|
|
||||||
assetGroup: immichAssetList,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
imageGridGroup.add(
|
|
||||||
ImageGrid(
|
|
||||||
assetGroup: immichAssetList,
|
|
||||||
sortedAssetGroup: sortedAssetList,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
lastMonth = currentMonth;
|
|
||||||
});
|
|
||||||
|
|
||||||
return DraggableScrollbar.semicircle(
|
|
||||||
backgroundColor: Theme.of(context).hintColor,
|
|
||||||
controller: scrollController,
|
|
||||||
heightScrollThumb: 48.0,
|
|
||||||
child: CustomScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
slivers: [...imageGridGroup],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const Text("No assets found");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
|
||||||
|
|
||||||
class ExperimentalSettings extends HookConsumerWidget {
|
class ExperimentalSettings extends HookConsumerWidget {
|
||||||
const ExperimentalSettings({
|
const ExperimentalSettings({
|
||||||
@@ -14,33 +9,6 @@ class ExperimentalSettings extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
|
||||||
|
|
||||||
final useExperimentalAssetGrid = useState(false);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
useExperimentalAssetGrid.value = appSettingService
|
|
||||||
.getSetting(AppSettingsEnum.useExperimentalAssetGrid);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
void changeUseExperimentalAssetGrid(bool status) {
|
|
||||||
useExperimentalAssetGrid.value = status;
|
|
||||||
appSettingService.setSetting(
|
|
||||||
AppSettingsEnum.useExperimentalAssetGrid,
|
|
||||||
status,
|
|
||||||
);
|
|
||||||
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: "settings_require_restart".tr(),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExpansionTile(
|
return ExpansionTile(
|
||||||
textColor: Theme.of(context).primaryColor,
|
textColor: Theme.of(context).primaryColor,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
@@ -55,25 +23,25 @@ class ExperimentalSettings extends HookConsumerWidget {
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
children: [
|
children: const [
|
||||||
SwitchListTile.adaptive(
|
// SwitchListTile.adaptive(
|
||||||
activeColor: Theme.of(context).primaryColor,
|
// activeColor: Theme.of(context).primaryColor,
|
||||||
title: const Text(
|
// title: const Text(
|
||||||
"experimental_settings_new_asset_list_title",
|
// "experimental_settings_new_asset_list_title",
|
||||||
style: TextStyle(
|
// style: TextStyle(
|
||||||
fontSize: 12,
|
// fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
// fontWeight: FontWeight.bold,
|
||||||
),
|
// ),
|
||||||
).tr(),
|
// ).tr(),
|
||||||
subtitle: const Text(
|
// subtitle: const Text(
|
||||||
"experimental_settings_new_asset_list_subtitle",
|
// "experimental_settings_new_asset_list_subtitle",
|
||||||
style: TextStyle(
|
// style: TextStyle(
|
||||||
fontSize: 12,
|
// fontSize: 12,
|
||||||
),
|
// ),
|
||||||
).tr(),
|
// ).tr(),
|
||||||
value: useExperimentalAssetGrid.value,
|
// value: useExperimentalAssetGrid.value,
|
||||||
onChanged: changeUseExperimentalAssetGrid,
|
// onChanged: changeUseExperimentalAssetGrid,
|
||||||
),
|
// ),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
const ThemeSetting(),
|
const ThemeSetting(),
|
||||||
const AssetListSettings(),
|
const AssetListSettings(),
|
||||||
if (Platform.isAndroid) const NotificationSetting(),
|
if (Platform.isAndroid) const NotificationSetting(),
|
||||||
const ExperimentalSettings(),
|
//const ExperimentalSettings(),
|
||||||
],
|
],
|
||||||
).toList(),
|
).toList(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
@@ -9,24 +10,50 @@ import 'package:photo_manager/photo_manager.dart';
|
|||||||
|
|
||||||
class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
||||||
final AssetService _assetService;
|
final AssetService _assetService;
|
||||||
|
final AssetCacheService _assetCacheService;
|
||||||
|
|
||||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||||
|
|
||||||
AssetNotifier(this._assetService) : super([]);
|
AssetNotifier(this._assetService, this._assetCacheService) : super([]);
|
||||||
|
|
||||||
|
_cacheState() {
|
||||||
|
_assetCacheService.put(state);
|
||||||
|
}
|
||||||
|
|
||||||
getAllAsset() async {
|
getAllAsset() async {
|
||||||
|
final stopwatch = Stopwatch();
|
||||||
|
|
||||||
|
|
||||||
|
if (await _assetCacheService.isValid() && state.isEmpty) {
|
||||||
|
stopwatch.start();
|
||||||
|
state = await _assetCacheService.get();
|
||||||
|
debugPrint("Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
stopwatch.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.start();
|
||||||
var allAssets = await _assetService.getAllAsset();
|
var allAssets = await _assetService.getAllAsset();
|
||||||
|
debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
stopwatch.reset();
|
||||||
|
|
||||||
if (allAssets != null) {
|
if (allAssets != null) {
|
||||||
state = allAssets;
|
state = allAssets;
|
||||||
|
|
||||||
|
stopwatch.start();
|
||||||
|
_cacheState();
|
||||||
|
debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
stopwatch.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAllAsset() {
|
clearAllAsset() {
|
||||||
state = [];
|
state = [];
|
||||||
|
_cacheState();
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewAssetUploaded(AssetResponseDto newAsset) {
|
onNewAssetUploaded(AssetResponseDto newAsset) {
|
||||||
state = [...state, newAsset];
|
state = [...state, newAsset];
|
||||||
|
_cacheState();
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAssets(Set<AssetResponseDto> deleteAssets) async {
|
deleteAssets(Set<AssetResponseDto> deleteAssets) async {
|
||||||
@@ -65,12 +92,15 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
|||||||
state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cacheState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetProvider =
|
final assetProvider =
|
||||||
StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
|
StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
|
||||||
return AssetNotifier(ref.watch(assetServiceProvider));
|
return AssetNotifier(
|
||||||
|
ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
||||||
|
|||||||
49
mobile/lib/shared/services/json_cache.dart
Normal file
49
mobile/lib/shared/services/json_cache.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
abstract class JsonCache<T> {
|
||||||
|
final String cacheFileName;
|
||||||
|
|
||||||
|
JsonCache(this.cacheFileName);
|
||||||
|
|
||||||
|
Future<File> _getCacheFile() async {
|
||||||
|
final basePath = await getTemporaryDirectory();
|
||||||
|
final basePathName = basePath.path;
|
||||||
|
|
||||||
|
final file = File("$basePathName/$cacheFileName.bin");
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isValid() async {
|
||||||
|
final file = await _getCacheFile();
|
||||||
|
return await file.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> invalidate() async {
|
||||||
|
final file = await _getCacheFile();
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> putRawData(dynamic data) async {
|
||||||
|
final jsonString = json.encode(data);
|
||||||
|
final file = await _getCacheFile();
|
||||||
|
|
||||||
|
if (!await file.exists()) {
|
||||||
|
await file.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
await file.writeAsString(jsonString);
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic readRawData() async {
|
||||||
|
final file = await _getCacheFile();
|
||||||
|
final data = await file.readAsString();
|
||||||
|
return json.decode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void put(T data);
|
||||||
|
Future<T> get();
|
||||||
|
}
|
||||||
@@ -29,9 +29,9 @@ class SplashScreenPage extends HookConsumerWidget {
|
|||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
// Resume backup (if enable) then navigate
|
// Resume backup (if enable) then navigate
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
AutoRouter.of(context).replace(const TabControllerRoute());
|
||||||
} else {
|
} else {
|
||||||
AutoRouter.of(context).push(const LoginRoute());
|
AutoRouter.of(context).replace(const LoginRoute());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class SplashScreenPage extends HookConsumerWidget {
|
|||||||
if (loginInfo?.isSaveLogin == true) {
|
if (loginInfo?.isSaveLogin == true) {
|
||||||
performLoggingIn();
|
performLoggingIn();
|
||||||
} else {
|
} else {
|
||||||
AutoRouter.of(context).push(const LoginRoute());
|
AutoRouter.of(context).replace(const LoginRoute());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
class TabControllerPage extends ConsumerWidget {
|
class TabControllerPage extends ConsumerWidget {
|
||||||
@@ -10,9 +11,7 @@ class TabControllerPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var isMultiSelectEnable =
|
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
|
||||||
|
|
||||||
return AutoTabsRouter(
|
return AutoTabsRouter(
|
||||||
routes: [
|
routes: [
|
||||||
const HomeRoute(),
|
const HomeRoute(),
|
||||||
@@ -22,17 +21,23 @@ class TabControllerPage extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
builder: (context, child, animation) {
|
builder: (context, child, animation) {
|
||||||
final tabsRouter = AutoTabsRouter.of(context);
|
final tabsRouter = AutoTabsRouter.of(context);
|
||||||
|
final appRouter = AutoRouter.of(context);
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
tabsRouter.setActiveIndex(0);
|
bool atHomeTab = tabsRouter.activeIndex == 0;
|
||||||
return false;
|
if (!atHomeTab) {
|
||||||
|
tabsRouter.setActiveIndex(0);
|
||||||
|
} else {
|
||||||
|
appRouter.navigateBack();
|
||||||
|
}
|
||||||
|
return atHomeTab;
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: FadeTransition(
|
body: FadeTransition(
|
||||||
opacity: animation,
|
opacity: animation,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
bottomNavigationBar: isMultiSelectEnable
|
bottomNavigationBar: multiselectEnabled
|
||||||
? null
|
? null
|
||||||
: BottomNavigationBar(
|
: BottomNavigationBar(
|
||||||
selectedLabelStyle: const TextStyle(
|
selectedLabelStyle: const TextStyle(
|
||||||
@@ -45,6 +50,7 @@ class TabControllerPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
currentIndex: tabsRouter.activeIndex,
|
currentIndex: tabsRouter.activeIndex,
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
tabsRouter.setActiveIndex(index);
|
tabsRouter.setActiveIndex(index);
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
primary: Colors.indigo,
|
backgroundColor: Colors.indigo,
|
||||||
onPrimary: Colors.grey[50],
|
foregroundColor: Colors.grey[50],
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 10,
|
vertical: 10,
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ String getImageUrl(final AssetResponseDto asset) {
|
|||||||
return '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false';
|
return '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false';
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getThumbnailUrl(final String id,
|
String _getThumbnailUrl(
|
||||||
{ThumbnailFormat type = ThumbnailFormat.WEBP}) {
|
final String id, {
|
||||||
|
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
||||||
|
}) {
|
||||||
final box = Hive.box(userInfoBox);
|
final box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
return '${box.get(serverEndpointKey)}/asset/thumbnail/${id}?format=${type.value}';
|
return '${box.get(serverEndpointKey)}/asset/thumbnail/$id?format=${type.value}';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ ThemeData immichDarkTheme = ThemeData(
|
|||||||
cardColor: Colors.grey[900],
|
cardColor: Colors.grey[900],
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
onPrimary: Colors.black87,
|
foregroundColor: Colors.black87,
|
||||||
primary: immichDarkThemePrimaryColor,
|
backgroundColor: immichDarkThemePrimaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -126,8 +126,8 @@ ThemeData immichLightTheme = ThemeData(
|
|||||||
),
|
),
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: Colors.indigo,
|
backgroundColor: Colors.indigo,
|
||||||
onPrimary: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -112,8 +112,10 @@ class ImmichCacheInfoRepository extends ImmichCacheRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CacheObject> insert(CacheObject cacheObject,
|
Future<CacheObject> insert(
|
||||||
{bool setTouchedToNow = true}) async {
|
CacheObject cacheObject, {
|
||||||
|
bool setTouchedToNow = true,
|
||||||
|
}) async {
|
||||||
int newId = keyLookupHiveBox.length == 0
|
int newId = keyLookupHiveBox.length == 0
|
||||||
? 0
|
? 0
|
||||||
: keyLookupHiveBox.values.reduce(max) + 1;
|
: keyLookupHiveBox.values.reduce(max) + 1;
|
||||||
@@ -144,8 +146,10 @@ class ImmichCacheInfoRepository extends ImmichCacheRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> update(CacheObject cacheObject,
|
Future<int> update(
|
||||||
{bool setTouchedToNow = true}) async {
|
CacheObject cacheObject, {
|
||||||
|
bool setTouchedToNow = true,
|
||||||
|
}) async {
|
||||||
if (cacheObject.id != null) {
|
if (cacheObject.id != null) {
|
||||||
cacheObjectLookupBox.put(cacheObject.id, cacheObject.toMap());
|
cacheObjectLookupBox.put(cacheObject.id, cacheObject.toMap());
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ doc/AssetTypeEnum.md
|
|||||||
doc/AuthenticationApi.md
|
doc/AuthenticationApi.md
|
||||||
doc/CheckDuplicateAssetDto.md
|
doc/CheckDuplicateAssetDto.md
|
||||||
doc/CheckDuplicateAssetResponseDto.md
|
doc/CheckDuplicateAssetResponseDto.md
|
||||||
|
doc/CheckExistingAssetsDto.md
|
||||||
|
doc/CheckExistingAssetsResponseDto.md
|
||||||
doc/CreateAlbumDto.md
|
doc/CreateAlbumDto.md
|
||||||
doc/CreateDeviceInfoDto.md
|
doc/CreateDeviceInfoDto.md
|
||||||
doc/CreateProfileImageResponseDto.md
|
doc/CreateProfileImageResponseDto.md
|
||||||
@@ -48,6 +50,7 @@ doc/SearchAssetDto.md
|
|||||||
doc/ServerInfoApi.md
|
doc/ServerInfoApi.md
|
||||||
doc/ServerInfoResponseDto.md
|
doc/ServerInfoResponseDto.md
|
||||||
doc/ServerPingResponse.md
|
doc/ServerPingResponse.md
|
||||||
|
doc/ServerStatsResponseDto.md
|
||||||
doc/ServerVersionReponseDto.md
|
doc/ServerVersionReponseDto.md
|
||||||
doc/SignUpDto.md
|
doc/SignUpDto.md
|
||||||
doc/SmartInfoResponseDto.md
|
doc/SmartInfoResponseDto.md
|
||||||
@@ -56,6 +59,7 @@ doc/TimeGroupEnum.md
|
|||||||
doc/UpdateAlbumDto.md
|
doc/UpdateAlbumDto.md
|
||||||
doc/UpdateDeviceInfoDto.md
|
doc/UpdateDeviceInfoDto.md
|
||||||
doc/UpdateUserDto.md
|
doc/UpdateUserDto.md
|
||||||
|
doc/UsageByUserDto.md
|
||||||
doc/UserApi.md
|
doc/UserApi.md
|
||||||
doc/UserCountResponseDto.md
|
doc/UserCountResponseDto.md
|
||||||
doc/UserResponseDto.md
|
doc/UserResponseDto.md
|
||||||
@@ -91,6 +95,8 @@ lib/model/asset_response_dto.dart
|
|||||||
lib/model/asset_type_enum.dart
|
lib/model/asset_type_enum.dart
|
||||||
lib/model/check_duplicate_asset_dto.dart
|
lib/model/check_duplicate_asset_dto.dart
|
||||||
lib/model/check_duplicate_asset_response_dto.dart
|
lib/model/check_duplicate_asset_response_dto.dart
|
||||||
|
lib/model/check_existing_assets_dto.dart
|
||||||
|
lib/model/check_existing_assets_response_dto.dart
|
||||||
lib/model/create_album_dto.dart
|
lib/model/create_album_dto.dart
|
||||||
lib/model/create_device_info_dto.dart
|
lib/model/create_device_info_dto.dart
|
||||||
lib/model/create_profile_image_response_dto.dart
|
lib/model/create_profile_image_response_dto.dart
|
||||||
@@ -117,6 +123,7 @@ lib/model/remove_assets_dto.dart
|
|||||||
lib/model/search_asset_dto.dart
|
lib/model/search_asset_dto.dart
|
||||||
lib/model/server_info_response_dto.dart
|
lib/model/server_info_response_dto.dart
|
||||||
lib/model/server_ping_response.dart
|
lib/model/server_ping_response.dart
|
||||||
|
lib/model/server_stats_response_dto.dart
|
||||||
lib/model/server_version_reponse_dto.dart
|
lib/model/server_version_reponse_dto.dart
|
||||||
lib/model/sign_up_dto.dart
|
lib/model/sign_up_dto.dart
|
||||||
lib/model/smart_info_response_dto.dart
|
lib/model/smart_info_response_dto.dart
|
||||||
@@ -125,7 +132,10 @@ lib/model/time_group_enum.dart
|
|||||||
lib/model/update_album_dto.dart
|
lib/model/update_album_dto.dart
|
||||||
lib/model/update_device_info_dto.dart
|
lib/model/update_device_info_dto.dart
|
||||||
lib/model/update_user_dto.dart
|
lib/model/update_user_dto.dart
|
||||||
|
lib/model/usage_by_user_dto.dart
|
||||||
lib/model/user_count_response_dto.dart
|
lib/model/user_count_response_dto.dart
|
||||||
lib/model/user_response_dto.dart
|
lib/model/user_response_dto.dart
|
||||||
lib/model/validate_access_token_response_dto.dart
|
lib/model/validate_access_token_response_dto.dart
|
||||||
pubspec.yaml
|
pubspec.yaml
|
||||||
|
test/check_existing_assets_dto_test.dart
|
||||||
|
test/check_existing_assets_response_dto_test.dart
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ Class | Method | HTTP request | Description
|
|||||||
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |
|
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |
|
||||||
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |
|
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |
|
||||||
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
|
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
|
||||||
|
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
|
||||||
*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset |
|
*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset |
|
||||||
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download |
|
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download |
|
||||||
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
|
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
|
||||||
@@ -102,6 +103,7 @@ Class | Method | HTTP request | Description
|
|||||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
|
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
|
||||||
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
|
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
|
||||||
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
|
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
|
||||||
|
*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats |
|
||||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||||
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image |
|
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image |
|
||||||
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user |
|
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user |
|
||||||
@@ -129,6 +131,8 @@ Class | Method | HTTP request | Description
|
|||||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||||
- [CheckDuplicateAssetDto](doc//CheckDuplicateAssetDto.md)
|
- [CheckDuplicateAssetDto](doc//CheckDuplicateAssetDto.md)
|
||||||
- [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md)
|
- [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md)
|
||||||
|
- [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
|
||||||
|
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
|
||||||
- [CreateAlbumDto](doc//CreateAlbumDto.md)
|
- [CreateAlbumDto](doc//CreateAlbumDto.md)
|
||||||
- [CreateDeviceInfoDto](doc//CreateDeviceInfoDto.md)
|
- [CreateDeviceInfoDto](doc//CreateDeviceInfoDto.md)
|
||||||
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
|
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
|
||||||
@@ -155,6 +159,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [SearchAssetDto](doc//SearchAssetDto.md)
|
- [SearchAssetDto](doc//SearchAssetDto.md)
|
||||||
- [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
|
- [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
|
||||||
- [ServerPingResponse](doc//ServerPingResponse.md)
|
- [ServerPingResponse](doc//ServerPingResponse.md)
|
||||||
|
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
||||||
- [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
|
- [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
|
||||||
- [SignUpDto](doc//SignUpDto.md)
|
- [SignUpDto](doc//SignUpDto.md)
|
||||||
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
|
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
|
||||||
@@ -163,6 +168,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
||||||
- [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
|
- [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
|
||||||
- [UpdateUserDto](doc//UpdateUserDto.md)
|
- [UpdateUserDto](doc//UpdateUserDto.md)
|
||||||
|
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||||
- [UserCountResponseDto](doc//UserCountResponseDto.md)
|
- [UserCountResponseDto](doc//UserCountResponseDto.md)
|
||||||
- [UserResponseDto](doc//UserResponseDto.md)
|
- [UserResponseDto](doc//UserResponseDto.md)
|
||||||
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
|
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ All URIs are relative to */api*
|
|||||||
Method | HTTP request | Description
|
Method | HTTP request | Description
|
||||||
------------- | ------------- | -------------
|
------------- | ------------- | -------------
|
||||||
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
|
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
|
||||||
|
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
|
||||||
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |
|
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |
|
||||||
[**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download |
|
[**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download |
|
||||||
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
|
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
|
||||||
@@ -76,6 +77,55 @@ Name | Type | Description | Notes
|
|||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **checkExistingAssets**
|
||||||
|
> CheckExistingAssetsResponseDto checkExistingAssets(checkExistingAssetsDto)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = AssetApi();
|
||||||
|
final checkExistingAssetsDto = CheckExistingAssetsDto(); // CheckExistingAssetsDto |
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.checkExistingAssets(checkExistingAssetsDto);
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AssetApi->checkExistingAssets: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**checkExistingAssetsDto** | [**CheckExistingAssetsDto**](CheckExistingAssetsDto.md)| |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**CheckExistingAssetsResponseDto**](CheckExistingAssetsResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **deleteAsset**
|
# **deleteAsset**
|
||||||
> List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto)
|
> List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto)
|
||||||
|
|
||||||
|
|||||||
16
mobile/openapi/doc/AssetCountResponseDto.md
Normal file
16
mobile/openapi/doc/AssetCountResponseDto.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# openapi.model.AssetCountResponseDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**photos** | **int** | |
|
||||||
|
**videos** | **int** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
16
mobile/openapi/doc/CheckExistingAssetsDto.md
Normal file
16
mobile/openapi/doc/CheckExistingAssetsDto.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# openapi.model.CheckExistingAssetsDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**deviceAssetIds** | **List<String>** | | [default to const []]
|
||||||
|
**deviceId** | **String** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
15
mobile/openapi/doc/CheckExistingAssetsResponseDto.md
Normal file
15
mobile/openapi/doc/CheckExistingAssetsResponseDto.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# openapi.model.CheckExistingAssetsResponseDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**existingIds** | **List<String>** | | [default to const []]
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ Method | HTTP request | Description
|
|||||||
------------- | ------------- | -------------
|
------------- | ------------- | -------------
|
||||||
[**getServerInfo**](ServerInfoApi.md#getserverinfo) | **GET** /server-info |
|
[**getServerInfo**](ServerInfoApi.md#getserverinfo) | **GET** /server-info |
|
||||||
[**getServerVersion**](ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
|
[**getServerVersion**](ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
|
||||||
|
[**getStats**](ServerInfoApi.md#getstats) | **GET** /server-info/stats |
|
||||||
[**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
[**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||||
|
|
||||||
|
|
||||||
@@ -88,6 +89,43 @@ No authorization required
|
|||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **getStats**
|
||||||
|
> ServerStatsResponseDto getStats()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final api_instance = ServerInfoApi();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.getStats();
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling ServerInfoApi->getStats: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
This endpoint does not need any parameter.
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**ServerStatsResponseDto**](ServerStatsResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **pingServer**
|
# **pingServer**
|
||||||
> ServerPingResponse pingServer()
|
> ServerPingResponse pingServer()
|
||||||
|
|
||||||
|
|||||||
20
mobile/openapi/doc/ServerStatsResponseDto.md
Normal file
20
mobile/openapi/doc/ServerStatsResponseDto.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# openapi.model.ServerStatsResponseDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**photos** | **int** | |
|
||||||
|
**videos** | **int** | |
|
||||||
|
**objects** | **int** | |
|
||||||
|
**usageRaw** | **int** | |
|
||||||
|
**usage** | **String** | |
|
||||||
|
**usageByUser** | [**List<UsageByUserDto>**](UsageByUserDto.md) | | [default to const []]
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
20
mobile/openapi/doc/UsageByUserDto.md
Normal file
20
mobile/openapi/doc/UsageByUserDto.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# openapi.model.UsageByUserDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**userId** | **String** | |
|
||||||
|
**objects** | **int** | |
|
||||||
|
**videos** | **int** | |
|
||||||
|
**photos** | **int** | |
|
||||||
|
**usageRaw** | **int** | |
|
||||||
|
**usage** | **String** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +49,8 @@ part 'model/asset_response_dto.dart';
|
|||||||
part 'model/asset_type_enum.dart';
|
part 'model/asset_type_enum.dart';
|
||||||
part 'model/check_duplicate_asset_dto.dart';
|
part 'model/check_duplicate_asset_dto.dart';
|
||||||
part 'model/check_duplicate_asset_response_dto.dart';
|
part 'model/check_duplicate_asset_response_dto.dart';
|
||||||
|
part 'model/check_existing_assets_dto.dart';
|
||||||
|
part 'model/check_existing_assets_response_dto.dart';
|
||||||
part 'model/create_album_dto.dart';
|
part 'model/create_album_dto.dart';
|
||||||
part 'model/create_device_info_dto.dart';
|
part 'model/create_device_info_dto.dart';
|
||||||
part 'model/create_profile_image_response_dto.dart';
|
part 'model/create_profile_image_response_dto.dart';
|
||||||
@@ -75,6 +77,7 @@ part 'model/remove_assets_dto.dart';
|
|||||||
part 'model/search_asset_dto.dart';
|
part 'model/search_asset_dto.dart';
|
||||||
part 'model/server_info_response_dto.dart';
|
part 'model/server_info_response_dto.dart';
|
||||||
part 'model/server_ping_response.dart';
|
part 'model/server_ping_response.dart';
|
||||||
|
part 'model/server_stats_response_dto.dart';
|
||||||
part 'model/server_version_reponse_dto.dart';
|
part 'model/server_version_reponse_dto.dart';
|
||||||
part 'model/sign_up_dto.dart';
|
part 'model/sign_up_dto.dart';
|
||||||
part 'model/smart_info_response_dto.dart';
|
part 'model/smart_info_response_dto.dart';
|
||||||
@@ -83,6 +86,7 @@ part 'model/time_group_enum.dart';
|
|||||||
part 'model/update_album_dto.dart';
|
part 'model/update_album_dto.dart';
|
||||||
part 'model/update_device_info_dto.dart';
|
part 'model/update_device_info_dto.dart';
|
||||||
part 'model/update_user_dto.dart';
|
part 'model/update_user_dto.dart';
|
||||||
|
part 'model/usage_by_user_dto.dart';
|
||||||
part 'model/user_count_response_dto.dart';
|
part 'model/user_count_response_dto.dart';
|
||||||
part 'model/user_response_dto.dart';
|
part 'model/user_response_dto.dart';
|
||||||
part 'model/validate_access_token_response_dto.dart';
|
part 'model/validate_access_token_response_dto.dart';
|
||||||
|
|||||||
@@ -72,6 +72,62 @@ class AssetApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [CheckExistingAssetsDto] checkExistingAssetsDto (required):
|
||||||
|
Future<Response> checkExistingAssetsWithHttpInfo(CheckExistingAssetsDto checkExistingAssetsDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/asset/exist';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = checkExistingAssetsDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [CheckExistingAssetsDto] checkExistingAssetsDto (required):
|
||||||
|
Future<CheckExistingAssetsResponseDto?> checkExistingAssets(CheckExistingAssetsDto checkExistingAssetsDto,) async {
|
||||||
|
final response = await checkExistingAssetsWithHttpInfo(checkExistingAssetsDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'CheckExistingAssetsResponseDto',) as CheckExistingAssetsResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'DELETE /asset' operation and returns the [Response].
|
/// Performs an HTTP 'DELETE /asset' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -98,6 +98,47 @@ class ServerInfoApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /server-info/stats' operation and returns the [Response].
|
||||||
|
Future<Response> getStatsWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/server-info/stats';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServerStatsResponseDto?> getStats() async {
|
||||||
|
final response = await getStatsWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerStatsResponseDto',) as ServerStatsResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /server-info/ping' operation and returns the [Response].
|
/// Performs an HTTP 'GET /server-info/ping' operation and returns the [Response].
|
||||||
Future<Response> pingServerWithHttpInfo() async {
|
Future<Response> pingServerWithHttpInfo() async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
|
|||||||
@@ -220,6 +220,10 @@ class ApiClient {
|
|||||||
return CheckDuplicateAssetDto.fromJson(value);
|
return CheckDuplicateAssetDto.fromJson(value);
|
||||||
case 'CheckDuplicateAssetResponseDto':
|
case 'CheckDuplicateAssetResponseDto':
|
||||||
return CheckDuplicateAssetResponseDto.fromJson(value);
|
return CheckDuplicateAssetResponseDto.fromJson(value);
|
||||||
|
case 'CheckExistingAssetsDto':
|
||||||
|
return CheckExistingAssetsDto.fromJson(value);
|
||||||
|
case 'CheckExistingAssetsResponseDto':
|
||||||
|
return CheckExistingAssetsResponseDto.fromJson(value);
|
||||||
case 'CreateAlbumDto':
|
case 'CreateAlbumDto':
|
||||||
return CreateAlbumDto.fromJson(value);
|
return CreateAlbumDto.fromJson(value);
|
||||||
case 'CreateDeviceInfoDto':
|
case 'CreateDeviceInfoDto':
|
||||||
@@ -272,6 +276,8 @@ class ApiClient {
|
|||||||
return ServerInfoResponseDto.fromJson(value);
|
return ServerInfoResponseDto.fromJson(value);
|
||||||
case 'ServerPingResponse':
|
case 'ServerPingResponse':
|
||||||
return ServerPingResponse.fromJson(value);
|
return ServerPingResponse.fromJson(value);
|
||||||
|
case 'ServerStatsResponseDto':
|
||||||
|
return ServerStatsResponseDto.fromJson(value);
|
||||||
case 'ServerVersionReponseDto':
|
case 'ServerVersionReponseDto':
|
||||||
return ServerVersionReponseDto.fromJson(value);
|
return ServerVersionReponseDto.fromJson(value);
|
||||||
case 'SignUpDto':
|
case 'SignUpDto':
|
||||||
@@ -288,6 +294,8 @@ class ApiClient {
|
|||||||
return UpdateDeviceInfoDto.fromJson(value);
|
return UpdateDeviceInfoDto.fromJson(value);
|
||||||
case 'UpdateUserDto':
|
case 'UpdateUserDto':
|
||||||
return UpdateUserDto.fromJson(value);
|
return UpdateUserDto.fromJson(value);
|
||||||
|
case 'UsageByUserDto':
|
||||||
|
return UsageByUserDto.fromJson(value);
|
||||||
case 'UserCountResponseDto':
|
case 'UserCountResponseDto':
|
||||||
return UserCountResponseDto.fromJson(value);
|
return UserCountResponseDto.fromJson(value);
|
||||||
case 'UserResponseDto':
|
case 'UserResponseDto':
|
||||||
|
|||||||
119
mobile/openapi/lib/model/asset_count_response_dto.dart
Normal file
119
mobile/openapi/lib/model/asset_count_response_dto.dart
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class AssetCountResponseDto {
|
||||||
|
/// Returns a new [AssetCountResponseDto] instance.
|
||||||
|
AssetCountResponseDto({
|
||||||
|
required this.photos,
|
||||||
|
required this.videos,
|
||||||
|
});
|
||||||
|
|
||||||
|
int photos;
|
||||||
|
|
||||||
|
int videos;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is AssetCountResponseDto &&
|
||||||
|
other.photos == photos &&
|
||||||
|
other.videos == videos;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(photos.hashCode) +
|
||||||
|
(videos.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AssetCountResponseDto[photos=$photos, videos=$videos]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_json[r'photos'] = photos;
|
||||||
|
_json[r'videos'] = videos;
|
||||||
|
return _json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [AssetCountResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static AssetCountResponseDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "AssetCountResponseDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "AssetCountResponseDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return AssetCountResponseDto(
|
||||||
|
photos: mapValueOfType<int>(json, r'photos')!,
|
||||||
|
videos: mapValueOfType<int>(json, r'videos')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<AssetCountResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <AssetCountResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = AssetCountResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, AssetCountResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, AssetCountResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = AssetCountResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of AssetCountResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<AssetCountResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<AssetCountResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = AssetCountResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'photos',
|
||||||
|
'videos',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
121
mobile/openapi/lib/model/check_existing_assets_dto.dart
Normal file
121
mobile/openapi/lib/model/check_existing_assets_dto.dart
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class CheckExistingAssetsDto {
|
||||||
|
/// Returns a new [CheckExistingAssetsDto] instance.
|
||||||
|
CheckExistingAssetsDto({
|
||||||
|
this.deviceAssetIds = const [],
|
||||||
|
required this.deviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> deviceAssetIds;
|
||||||
|
|
||||||
|
String deviceId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsDto &&
|
||||||
|
other.deviceAssetIds == deviceAssetIds &&
|
||||||
|
other.deviceId == deviceId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(deviceAssetIds.hashCode) +
|
||||||
|
(deviceId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'CheckExistingAssetsDto[deviceAssetIds=$deviceAssetIds, deviceId=$deviceId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_json[r'deviceAssetIds'] = deviceAssetIds;
|
||||||
|
_json[r'deviceId'] = deviceId;
|
||||||
|
return _json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [CheckExistingAssetsDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static CheckExistingAssetsDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "CheckExistingAssetsDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "CheckExistingAssetsDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return CheckExistingAssetsDto(
|
||||||
|
deviceAssetIds: json[r'deviceAssetIds'] is List
|
||||||
|
? (json[r'deviceAssetIds'] as List).cast<String>()
|
||||||
|
: const [],
|
||||||
|
deviceId: mapValueOfType<String>(json, r'deviceId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<CheckExistingAssetsDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <CheckExistingAssetsDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = CheckExistingAssetsDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, CheckExistingAssetsDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, CheckExistingAssetsDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = CheckExistingAssetsDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of CheckExistingAssetsDto-objects as value to a dart map
|
||||||
|
static Map<String, List<CheckExistingAssetsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<CheckExistingAssetsDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = CheckExistingAssetsDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'deviceAssetIds',
|
||||||
|
'deviceId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
113
mobile/openapi/lib/model/check_existing_assets_response_dto.dart
Normal file
113
mobile/openapi/lib/model/check_existing_assets_response_dto.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class CheckExistingAssetsResponseDto {
|
||||||
|
/// Returns a new [CheckExistingAssetsResponseDto] instance.
|
||||||
|
CheckExistingAssetsResponseDto({
|
||||||
|
this.existingIds = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> existingIds;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsResponseDto &&
|
||||||
|
other.existingIds == existingIds;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(existingIds.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'CheckExistingAssetsResponseDto[existingIds=$existingIds]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_json[r'existingIds'] = existingIds;
|
||||||
|
return _json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [CheckExistingAssetsResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static CheckExistingAssetsResponseDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "CheckExistingAssetsResponseDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "CheckExistingAssetsResponseDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return CheckExistingAssetsResponseDto(
|
||||||
|
existingIds: json[r'existingIds'] is List
|
||||||
|
? (json[r'existingIds'] as List).cast<String>()
|
||||||
|
: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<CheckExistingAssetsResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <CheckExistingAssetsResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = CheckExistingAssetsResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, CheckExistingAssetsResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, CheckExistingAssetsResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = CheckExistingAssetsResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of CheckExistingAssetsResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<CheckExistingAssetsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<CheckExistingAssetsResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = CheckExistingAssetsResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'existingIds',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
151
mobile/openapi/lib/model/server_stats_response_dto.dart
Normal file
151
mobile/openapi/lib/model/server_stats_response_dto.dart
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class ServerStatsResponseDto {
|
||||||
|
/// Returns a new [ServerStatsResponseDto] instance.
|
||||||
|
ServerStatsResponseDto({
|
||||||
|
required this.photos,
|
||||||
|
required this.videos,
|
||||||
|
required this.objects,
|
||||||
|
required this.usageRaw,
|
||||||
|
required this.usage,
|
||||||
|
this.usageByUser = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
int photos;
|
||||||
|
|
||||||
|
int videos;
|
||||||
|
|
||||||
|
int objects;
|
||||||
|
|
||||||
|
int usageRaw;
|
||||||
|
|
||||||
|
String usage;
|
||||||
|
|
||||||
|
List<UsageByUserDto> usageByUser;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is ServerStatsResponseDto &&
|
||||||
|
other.photos == photos &&
|
||||||
|
other.videos == videos &&
|
||||||
|
other.objects == objects &&
|
||||||
|
other.usageRaw == usageRaw &&
|
||||||
|
other.usage == usage &&
|
||||||
|
other.usageByUser == usageByUser;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(photos.hashCode) +
|
||||||
|
(videos.hashCode) +
|
||||||
|
(objects.hashCode) +
|
||||||
|
(usageRaw.hashCode) +
|
||||||
|
(usage.hashCode) +
|
||||||
|
(usageByUser.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ServerStatsResponseDto[photos=$photos, videos=$videos, objects=$objects, usageRaw=$usageRaw, usage=$usage, usageByUser=$usageByUser]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_json[r'photos'] = photos;
|
||||||
|
_json[r'videos'] = videos;
|
||||||
|
_json[r'objects'] = objects;
|
||||||
|
_json[r'usageRaw'] = usageRaw;
|
||||||
|
_json[r'usage'] = usage;
|
||||||
|
_json[r'usageByUser'] = usageByUser;
|
||||||
|
return _json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [ServerStatsResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static ServerStatsResponseDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "ServerStatsResponseDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "ServerStatsResponseDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return ServerStatsResponseDto(
|
||||||
|
photos: mapValueOfType<int>(json, r'photos')!,
|
||||||
|
videos: mapValueOfType<int>(json, r'videos')!,
|
||||||
|
objects: mapValueOfType<int>(json, r'objects')!,
|
||||||
|
usageRaw: mapValueOfType<int>(json, r'usageRaw')!,
|
||||||
|
usage: mapValueOfType<String>(json, r'usage')!,
|
||||||
|
usageByUser: UsageByUserDto.listFromJson(json[r'usageByUser'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ServerStatsResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <ServerStatsResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = ServerStatsResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, ServerStatsResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, ServerStatsResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = ServerStatsResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of ServerStatsResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<ServerStatsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<ServerStatsResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = ServerStatsResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'photos',
|
||||||
|
'videos',
|
||||||
|
'objects',
|
||||||
|
'usageRaw',
|
||||||
|
'usage',
|
||||||
|
'usageByUser',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
151
mobile/openapi/lib/model/usage_by_user_dto.dart
Normal file
151
mobile/openapi/lib/model/usage_by_user_dto.dart
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class UsageByUserDto {
|
||||||
|
/// Returns a new [UsageByUserDto] instance.
|
||||||
|
UsageByUserDto({
|
||||||
|
required this.userId,
|
||||||
|
required this.objects,
|
||||||
|
required this.videos,
|
||||||
|
required this.photos,
|
||||||
|
required this.usageRaw,
|
||||||
|
required this.usage,
|
||||||
|
});
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
|
||||||
|
int objects;
|
||||||
|
|
||||||
|
int videos;
|
||||||
|
|
||||||
|
int photos;
|
||||||
|
|
||||||
|
int usageRaw;
|
||||||
|
|
||||||
|
String usage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is UsageByUserDto &&
|
||||||
|
other.userId == userId &&
|
||||||
|
other.objects == objects &&
|
||||||
|
other.videos == videos &&
|
||||||
|
other.photos == photos &&
|
||||||
|
other.usageRaw == usageRaw &&
|
||||||
|
other.usage == usage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(userId.hashCode) +
|
||||||
|
(objects.hashCode) +
|
||||||
|
(videos.hashCode) +
|
||||||
|
(photos.hashCode) +
|
||||||
|
(usageRaw.hashCode) +
|
||||||
|
(usage.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'UsageByUserDto[userId=$userId, objects=$objects, videos=$videos, photos=$photos, usageRaw=$usageRaw, usage=$usage]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_json[r'userId'] = userId;
|
||||||
|
_json[r'objects'] = objects;
|
||||||
|
_json[r'videos'] = videos;
|
||||||
|
_json[r'photos'] = photos;
|
||||||
|
_json[r'usageRaw'] = usageRaw;
|
||||||
|
_json[r'usage'] = usage;
|
||||||
|
return _json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [UsageByUserDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static UsageByUserDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "UsageByUserDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "UsageByUserDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return UsageByUserDto(
|
||||||
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
|
objects: mapValueOfType<int>(json, r'objects')!,
|
||||||
|
videos: mapValueOfType<int>(json, r'videos')!,
|
||||||
|
photos: mapValueOfType<int>(json, r'photos')!,
|
||||||
|
usageRaw: mapValueOfType<int>(json, r'usageRaw')!,
|
||||||
|
usage: mapValueOfType<String>(json, r'usage')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<UsageByUserDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <UsageByUserDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = UsageByUserDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, UsageByUserDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, UsageByUserDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = UsageByUserDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of UsageByUserDto-objects as value to a dart map
|
||||||
|
static Map<String, List<UsageByUserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<UsageByUserDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = UsageByUserDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'userId',
|
||||||
|
'objects',
|
||||||
|
'videos',
|
||||||
|
'photos',
|
||||||
|
'usageRaw',
|
||||||
|
'usage',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
32
mobile/openapi/test/asset_count_response_dto_test.dart
Normal file
32
mobile/openapi/test/asset_count_response_dto_test.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for AssetCountResponseDto
|
||||||
|
void main() {
|
||||||
|
// final instance = AssetCountResponseDto();
|
||||||
|
|
||||||
|
group('test AssetCountResponseDto', () {
|
||||||
|
// int photos
|
||||||
|
test('to test the property `photos`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// int videos
|
||||||
|
test('to test the property `videos`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
32
mobile/openapi/test/check_existing_assets_dto_test.dart
Normal file
32
mobile/openapi/test/check_existing_assets_dto_test.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for CheckExistingAssetsDto
|
||||||
|
void main() {
|
||||||
|
// final instance = CheckExistingAssetsDto();
|
||||||
|
|
||||||
|
group('test CheckExistingAssetsDto', () {
|
||||||
|
// List<String> deviceAssetIds (default value: const [])
|
||||||
|
test('to test the property `deviceAssetIds`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// String deviceId
|
||||||
|
test('to test the property `deviceId`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for CheckExistingAssetsResponseDto
|
||||||
|
void main() {
|
||||||
|
// final instance = CheckExistingAssetsResponseDto();
|
||||||
|
|
||||||
|
group('test CheckExistingAssetsResponseDto', () {
|
||||||
|
// List<String> existingIds (default value: const [])
|
||||||
|
test('to test the property `existingIds`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
42
mobile/openapi/test/server_stats_response_dto_test.dart
Normal file
42
mobile/openapi/test/server_stats_response_dto_test.dart
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for ServerStatsResponseDto
|
||||||
|
void main() {
|
||||||
|
// final instance = ServerStatsResponseDto();
|
||||||
|
|
||||||
|
group('test ServerStatsResponseDto', () {
|
||||||
|
// int photos
|
||||||
|
test('to test the property `photos`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// int videos
|
||||||
|
test('to test the property `videos`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// int objects
|
||||||
|
test('to test the property `objects`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// UsagePerUser diskUsagesByUser
|
||||||
|
test('to test the property `diskUsagesByUser`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
42
mobile/openapi/test/usage_by_user_dto_test.dart
Normal file
42
mobile/openapi/test/usage_by_user_dto_test.dart
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for UsageByUserDto
|
||||||
|
void main() {
|
||||||
|
// final instance = UsageByUserDto();
|
||||||
|
|
||||||
|
group('test UsageByUserDto', () {
|
||||||
|
// int usageRaw
|
||||||
|
test('to test the property `usageRaw`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// num objects
|
||||||
|
test('to test the property `objects`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// num videos
|
||||||
|
test('to test the property `videos`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// num images
|
||||||
|
test('to test the property `images`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.31.0+49
|
version: 1.33.0+52
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
|||||||
159
mobile/test/asset_grid_data_structure_test.dart
Normal file
159
mobile/test/asset_grid_data_structure_test.dart
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final List<AssetResponseDto> testAssets = [];
|
||||||
|
|
||||||
|
for (int i = 0; i < 150; i++) {
|
||||||
|
int month = i ~/ 31;
|
||||||
|
int day = (i % 31).toInt();
|
||||||
|
|
||||||
|
DateTime date = DateTime(2022, month, day);
|
||||||
|
|
||||||
|
testAssets.add(AssetResponseDto(
|
||||||
|
type: AssetTypeEnum.IMAGE,
|
||||||
|
id: '$i',
|
||||||
|
deviceAssetId: '',
|
||||||
|
ownerId: '',
|
||||||
|
deviceId: '',
|
||||||
|
originalPath: '',
|
||||||
|
resizePath: '',
|
||||||
|
createdAt: date.toIso8601String(),
|
||||||
|
modifiedAt: date.toIso8601String(),
|
||||||
|
isFavorite: false,
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
duration: '',
|
||||||
|
webpPath: '',
|
||||||
|
encodedVideoPath: '',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, List<AssetResponseDto>> groups = {
|
||||||
|
'2022-01-05': testAssets.sublist(0, 5).map((e) {
|
||||||
|
e.createdAt = DateTime(2022, 1, 5).toIso8601String();
|
||||||
|
return e;
|
||||||
|
}).toList(),
|
||||||
|
'2022-01-10': testAssets.sublist(5, 10).map((e) {
|
||||||
|
e.createdAt = DateTime(2022, 1, 10).toIso8601String();
|
||||||
|
return e;
|
||||||
|
}).toList(),
|
||||||
|
'2022-02-17': testAssets.sublist(10, 15).map((e) {
|
||||||
|
e.createdAt = DateTime(2022, 2, 17).toIso8601String();
|
||||||
|
return e;
|
||||||
|
}).toList(),
|
||||||
|
'2022-10-15': testAssets.sublist(15, 30).map((e) {
|
||||||
|
e.createdAt = DateTime(2022, 10, 15).toIso8601String();
|
||||||
|
return e;
|
||||||
|
}).toList()
|
||||||
|
};
|
||||||
|
|
||||||
|
group('Asset only list', () {
|
||||||
|
test('items < itemsPerRow', () {
|
||||||
|
final assets = testAssets.sublist(0, 2);
|
||||||
|
final renderList = assetsToRenderList(assets, 3);
|
||||||
|
|
||||||
|
expect(renderList.length, 1);
|
||||||
|
expect(renderList[0].assetRow!.assets.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('items = itemsPerRow', () {
|
||||||
|
final assets = testAssets.sublist(0, 3);
|
||||||
|
final renderList = assetsToRenderList(assets, 3);
|
||||||
|
|
||||||
|
expect(renderList.length, 1);
|
||||||
|
expect(renderList[0].assetRow!.assets.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('items > itemsPerRow', () {
|
||||||
|
final assets = testAssets.sublist(0, 20);
|
||||||
|
final renderList = assetsToRenderList(assets, 3);
|
||||||
|
|
||||||
|
expect(renderList.length, 7);
|
||||||
|
expect(renderList[6].assetRow!.assets.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('items > itemsPerRow partition 4', () {
|
||||||
|
final assets = testAssets.sublist(0, 21);
|
||||||
|
final renderList = assetsToRenderList(assets, 4);
|
||||||
|
|
||||||
|
expect(renderList.length, 6);
|
||||||
|
expect(renderList[5].assetRow!.assets.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('items > itemsPerRow check ids', () {
|
||||||
|
final assets = testAssets.sublist(0, 21);
|
||||||
|
final renderList = assetsToRenderList(assets, 3);
|
||||||
|
|
||||||
|
expect(renderList.length, 7);
|
||||||
|
expect(renderList[6].assetRow!.assets.length, 3);
|
||||||
|
expect(renderList[0].assetRow!.assets[0].id, '0');
|
||||||
|
expect(renderList[1].assetRow!.assets[1].id, '4');
|
||||||
|
expect(renderList[3].assetRow!.assets[2].id, '11');
|
||||||
|
expect(renderList[6].assetRow!.assets[2].id, '20');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Test grouped', () {
|
||||||
|
test('test grouped check months', () {
|
||||||
|
final renderList = assetGroupsToRenderList(groups, 3);
|
||||||
|
|
||||||
|
// Jan
|
||||||
|
// Day 1
|
||||||
|
// 5 Assets => 2 Rows
|
||||||
|
// Day 2
|
||||||
|
// 5 Assets => 2 Rows
|
||||||
|
// Feb
|
||||||
|
// Day 1
|
||||||
|
// 5 Assets => 2 Rows
|
||||||
|
// Oct
|
||||||
|
// Day 1
|
||||||
|
// 15 Assets => 5 Rows
|
||||||
|
expect(renderList.length, 18);
|
||||||
|
expect(renderList[0].type, RenderAssetGridElementType.monthTitle);
|
||||||
|
expect(renderList[0].date.month, 1);
|
||||||
|
expect(renderList[7].type, RenderAssetGridElementType.monthTitle);
|
||||||
|
expect(renderList[7].date.month, 2);
|
||||||
|
expect(renderList[11].type, RenderAssetGridElementType.monthTitle);
|
||||||
|
expect(renderList[11].date.month, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test grouped check types', () {
|
||||||
|
final renderList = assetGroupsToRenderList(groups, 5);
|
||||||
|
|
||||||
|
// Jan
|
||||||
|
// Day 1
|
||||||
|
// 5 Assets
|
||||||
|
// Day 2
|
||||||
|
// 5 Assets
|
||||||
|
// Feb
|
||||||
|
// Day 1
|
||||||
|
// 5 Assets
|
||||||
|
// Oct
|
||||||
|
// Day 1
|
||||||
|
// 15 Assets => 3 Rows
|
||||||
|
|
||||||
|
final types = [
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
RenderAssetGridElementType.dayTitle,
|
||||||
|
RenderAssetGridElementType.assetRow,
|
||||||
|
RenderAssetGridElementType.dayTitle,
|
||||||
|
RenderAssetGridElementType.assetRow,
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
RenderAssetGridElementType.dayTitle,
|
||||||
|
RenderAssetGridElementType.assetRow,
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
RenderAssetGridElementType.dayTitle,
|
||||||
|
RenderAssetGridElementType.assetRow,
|
||||||
|
RenderAssetGridElementType.assetRow,
|
||||||
|
RenderAssetGridElementType.assetRow
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(renderList.length, types.length);
|
||||||
|
|
||||||
|
for (int i = 0; i < renderList.length; i++) {
|
||||||
|
expect(renderList[i].type, types[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
FROM docker.io/nginxinc/nginx-unprivileged:latest
|
FROM docker.io/nginxinc/nginx-unprivileged:latest
|
||||||
|
|
||||||
|
COPY LICENSE /licenses/LICENSE.txt
|
||||||
|
COPY LICENSE /LICENSE
|
||||||
|
|
||||||
COPY nginx.conf "/etc/nginx/nginx.conf"
|
COPY nginx.conf "/etc/nginx/nginx.conf"
|
||||||
|
|
||||||
CMD nginx -g "daemon off;"
|
CMD nginx -g "daemon off;"
|
||||||
21
nginx/LICENSE
Normal file
21
nginx/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Hau Tran
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
upload/
|
upload/
|
||||||
dist/
|
dist/
|
||||||
|
coverage/
|
||||||
.reverse-geocoding-dump
|
.reverse-geocoding-dump
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# Build stage
|
|
||||||
FROM node:16-alpine3.14 as builder
|
FROM node:16-alpine3.14 as builder
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@@ -12,16 +11,18 @@ COPY . .
|
|||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
# Prod stage
|
# Prod stage
|
||||||
FROM node:16-alpine3.14
|
FROM node:16-alpine3.14
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
COPY LICENSE /licenses/LICENSE.txt
|
||||||
|
COPY LICENSE /LICENSE
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
COPY start-server.sh start-microservices.sh ./
|
COPY start-server.sh start-microservices.sh ./
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/app/dist \
|
RUN mkdir -p /usr/src/app/dist \
|
||||||
|
&& mkdir /usr/src/app/.reverse-geocoding-dump \
|
||||||
&& apk add --no-cache libheif vips ffmpeg
|
&& apk add --no-cache libheif vips ffmpeg
|
||||||
|
|
||||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||||
@@ -29,6 +30,11 @@ COPY --from=builder /usr/src/app/dist ./dist
|
|||||||
|
|
||||||
RUN npm prune --production
|
RUN npm prune --production
|
||||||
|
|
||||||
|
RUN chown -R node:0 /usr/src/app \
|
||||||
|
&& chmod -R g=u /usr/src/app
|
||||||
|
|
||||||
|
RUN addgroup node root
|
||||||
|
|
||||||
VOLUME /usr/src/app/upload
|
VOLUME /usr/src/app/upload
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
|
|||||||
21
server/LICENSE
Normal file
21
server/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Hau Tran
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -137,6 +137,7 @@ describe('Album service', () => {
|
|||||||
getAssetWithNoEXIF: jest.fn(),
|
getAssetWithNoEXIF: jest.fn(),
|
||||||
getAssetWithNoThumbnail: jest.fn(),
|
getAssetWithNoThumbnail: jest.fn(),
|
||||||
getAssetWithNoSmartInfo: jest.fn(),
|
getAssetWithNoSmartInfo: jest.fn(),
|
||||||
|
getExistingAssets: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sut = new AlbumService(albumRepositoryMock, assetRepositoryMock);
|
sut = new AlbumService(albumRepositoryMock, assetRepositoryMock);
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group
|
|||||||
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
|
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||||
|
import { In } from 'typeorm/find-options/operator/In';
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
create(
|
create(
|
||||||
@@ -32,6 +35,7 @@ export interface IAssetRepository {
|
|||||||
getAssetWithNoThumbnail(): Promise<AssetEntity[]>;
|
getAssetWithNoThumbnail(): Promise<AssetEntity[]>;
|
||||||
getAssetWithNoEXIF(): Promise<AssetEntity[]>;
|
getAssetWithNoEXIF(): Promise<AssetEntity[]>;
|
||||||
getAssetWithNoSmartInfo(): Promise<AssetEntity[]>;
|
getAssetWithNoSmartInfo(): Promise<AssetEntity[]>;
|
||||||
|
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<CheckExistingAssetsResponseDto>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
|
export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
|
||||||
@@ -279,4 +283,17 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
relations: ['exifInfo'],
|
relations: ['exifInfo'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<CheckExistingAssetsResponseDto> {
|
||||||
|
const existingAssets = await this.assetRepository.find({
|
||||||
|
select: {deviceAssetId: true},
|
||||||
|
where: {
|
||||||
|
deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds),
|
||||||
|
deviceId: checkDuplicateAssetDto.deviceId,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return new CheckExistingAssetsResponseDto(existingAssets.map(a => a.deviceAssetId));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-buck
|
|||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||||
import { QueryFailedError } from 'typeorm';
|
import { QueryFailedError } from 'typeorm';
|
||||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
|
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -74,6 +76,7 @@ export class AssetController {
|
|||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
||||||
|
@Response({ passthrough: true }) res: Res,
|
||||||
): Promise<AssetFileUploadResponseDto> {
|
): Promise<AssetFileUploadResponseDto> {
|
||||||
const checksum = await this.assetService.calculateChecksum(file.path);
|
const checksum = await this.assetService.calculateChecksum(file.path);
|
||||||
|
|
||||||
@@ -111,6 +114,7 @@ export class AssetController {
|
|||||||
|
|
||||||
if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') {
|
if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') {
|
||||||
const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum);
|
const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum);
|
||||||
|
res.status(200); // normal POST is 201. we use 200 to indicate the asset already exists
|
||||||
return new AssetFileUploadResponseDto(existedAsset.id);
|
return new AssetFileUploadResponseDto(existedAsset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +186,7 @@ export class AssetController {
|
|||||||
async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||||
return this.assetService.getAssetCountByUserId(authUser);
|
return this.assetService.getAssetCountByUserId(authUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
*/
|
*/
|
||||||
@@ -253,4 +258,16 @@ export class AssetController {
|
|||||||
): Promise<CheckDuplicateAssetResponseDto> {
|
): Promise<CheckDuplicateAssetResponseDto> {
|
||||||
return await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto);
|
return await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||||
|
*/
|
||||||
|
@Post('/exist')
|
||||||
|
@HttpCode(200)
|
||||||
|
async checkExistingAssets(
|
||||||
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
|
@Body(ValidationPipe) checkExistingAssetsDto: CheckExistingAssetsDto,
|
||||||
|
): Promise<CheckExistingAssetsResponseDto> {
|
||||||
|
return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user