Compare commits
92 Commits
v1.16.0_23
...
v1.20.2_30
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28c7736ecd | ||
|
|
f881981c44 | ||
|
|
953d18e795 | ||
|
|
b45024a97e | ||
|
|
3dcdfa0166 | ||
|
|
2079583866 | ||
|
|
b68358766b | ||
|
|
cf2b9eddfa | ||
|
|
8c184dc4d4 | ||
|
|
e8d1f89a47 | ||
|
|
0e85b0fd8f | ||
|
|
f7dc916e80 | ||
|
|
03e7a254a2 | ||
|
|
0ac9fe5a54 | ||
|
|
dc61fd925f | ||
|
|
2aea08726f | ||
|
|
746bec908b | ||
|
|
8102e3b3f5 | ||
|
|
1ba998aa68 | ||
|
|
2de34f70ce | ||
|
|
8b9fd67d6f | ||
|
|
97238a1621 | ||
|
|
ef4136d327 | ||
|
|
6dbca8d478 | ||
|
|
a305db9e6f | ||
|
|
59c1ea3097 | ||
|
|
03457f5d32 | ||
|
|
2336a6159c | ||
|
|
e4c4b53fcd | ||
|
|
83cbf51704 | ||
|
|
2ebb755f00 | ||
|
|
ec1c3a86f5 | ||
|
|
969f770df0 | ||
|
|
9c3f848fa8 | ||
|
|
1ea6425cd1 | ||
|
|
052db5d748 | ||
|
|
a35460cb84 | ||
|
|
ae93bbe2a7 | ||
|
|
3b97c7729b | ||
|
|
6021124688 | ||
|
|
1d34976dd0 | ||
|
|
02bde51caf | ||
|
|
bef1e2e3db | ||
|
|
be3e3e5d7e | ||
|
|
c028c7db4e | ||
|
|
c129023821 | ||
|
|
cbdb8fa51f | ||
|
|
c6ecfb679a | ||
|
|
5d03e9bda8 | ||
|
|
d8b26c6da8 | ||
|
|
2e61cf3183 | ||
|
|
45e2335b86 | ||
|
|
2bbc44c5ab | ||
|
|
012428416d | ||
|
|
7134f93eb8 | ||
|
|
1887b5a860 | ||
|
|
ef17668871 | ||
|
|
e9909b179a | ||
|
|
09f8bdef6d | ||
|
|
2a9b09f359 | ||
|
|
1f6a3ccac7 | ||
|
|
1f40fc1de9 | ||
|
|
20b94ef0bb | ||
|
|
72c334e5e0 | ||
|
|
e7f35822af | ||
|
|
bd2152d568 | ||
|
|
b1d7ef03e2 | ||
|
|
aa74417d11 | ||
|
|
229b009b7f | ||
|
|
bece6253d5 | ||
|
|
ae7e582ec8 | ||
|
|
d69470e207 | ||
|
|
c60e852226 | ||
|
|
a205478a29 | ||
|
|
22d30522e1 | ||
|
|
19b1fad274 | ||
|
|
9a6dfacf9b | ||
|
|
7f236c5b18 | ||
|
|
25985c732d | ||
|
|
9ce50b7e3d | ||
|
|
f5e93a8179 | ||
|
|
2b5cef156c | ||
|
|
f3032f74a4 | ||
|
|
58ec7553ea | ||
|
|
357f7d1c31 | ||
|
|
e6d30d72fa | ||
|
|
355038a91a | ||
|
|
97d9b80baa | ||
|
|
b6814fad57 | ||
|
|
7586c65103 | ||
|
|
633170d743 | ||
|
|
c5be7827c3 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: alextran1502
|
github: alextran1502
|
||||||
custom: https://www.buymeacoffee.com/altran1502?new=1
|
custom: https://www.buymeacoffee.com/altran1502
|
||||||
|
|||||||
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -16,8 +16,11 @@ Note: Please search to see if an issue already exists for the bug you encountere
|
|||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**Task List**
|
**Task List**
|
||||||
|
|
||||||
|
*Please complete the task list below. We need this information to help us reproduce the bug or point out problems in your setup. You are not providing enough info may delay our effort to help you.*
|
||||||
|
|
||||||
- [ ] I have read thoroughly the README setup and installation instructions.
|
- [ ] I have read thoroughly the README setup and installation instructions.
|
||||||
- [ ] If my setup is different, I have included my docker-compose file.
|
- [ ] I have included my `docker-compose` file.
|
||||||
- [ ] I have included my redacted `.env` file.
|
- [ ] I have included my redacted `.env` file.
|
||||||
- [ ] I have included information on my machine, and environment.
|
- [ ] I have included information on my machine, and environment.
|
||||||
|
|
||||||
@@ -34,13 +37,10 @@ A clear and concise description of what you expected to happen.
|
|||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**System**
|
||||||
- OS: [e.g. iOS]
|
- Phone OS [iOS, Android]: `<version>`
|
||||||
|
- Server Version: `<version>`
|
||||||
**Smartphone (please complete the following information):**
|
- Mobile App Version: `<version>`
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|||||||
10
.github/workflows/build_push_docker_latest.yml
vendored
10
.github/workflows/build_push_docker_latest.yml
vendored
@@ -27,7 +27,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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -55,11 +55,11 @@ 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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./machine-learning
|
context: ./machine-learning
|
||||||
file: ./machine-learning/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-machine-learning:latest
|
altran1502/immich-machine-learning:latest
|
||||||
@@ -82,7 +82,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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
@@ -110,7 +110,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.0.0
|
uses: docker/build-push-action@v3.1.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
@@ -24,17 +24,18 @@ jobs:
|
|||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2.0.0
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ github.repository == 'immich-app/immich' }}
|
||||||
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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name == 'pull_request' }}
|
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-server:staging
|
altran1502/immich-server:staging
|
||||||
|
|
||||||
@@ -52,17 +53,18 @@ jobs:
|
|||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2.0.0
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ github.repository == 'immich-app/immich' }}
|
||||||
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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./machine-learning
|
context: ./machine-learning
|
||||||
file: ./machine-learning/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name == 'pull_request' }}
|
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-machine-learning:staging
|
altran1502/immich-machine-learning:staging
|
||||||
|
|
||||||
@@ -79,18 +81,19 @@ jobs:
|
|||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2.0.0
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ github.repository == 'immich-app/immich' }}
|
||||||
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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
target: prod
|
target: prod
|
||||||
push: ${{ github.event_name == 'pull_request' }}
|
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-web:staging
|
altran1502/immich-web:staging
|
||||||
|
|
||||||
@@ -107,16 +110,17 @@ jobs:
|
|||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2.0.0
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ github.repository == 'immich-app/immich' }}
|
||||||
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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
file: ./nginx/Dockerfile
|
file: ./nginx/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name == 'pull_request' }}
|
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-proxy:staging
|
altran1502/immich-proxy:staging
|
||||||
|
|||||||
10
.github/workflows/build_push_server_release.yml
vendored
10
.github/workflows/build_push_server_release.yml
vendored
@@ -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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -68,11 +68,11 @@ 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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./machine-learning
|
context: ./machine-learning
|
||||||
file: ./machine-learning/Dockerfile
|
file: ./machine-learning/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
|
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
|
||||||
@@ -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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
file: ./web/Dockerfile
|
file: ./web/Dockerfile
|
||||||
@@ -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.0.0
|
uses: docker/build-push-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
file: ./nginx/Dockerfile
|
file: ./nginx/Dockerfile
|
||||||
|
|||||||
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -1,5 +1,6 @@
|
|||||||
name: Test
|
name: Test
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
push: { branches: master }
|
push: { branches: master }
|
||||||
|
|
||||||
@@ -14,4 +15,4 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Run Immich Server 2E2 Test
|
- name: Run Immich Server 2E2 Test
|
||||||
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich_server_test
|
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
|
||||||
|
|||||||
16
Makefile
16
Makefile
@@ -1,20 +1,26 @@
|
|||||||
dev:
|
dev:
|
||||||
docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
dev-update:
|
||||||
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
dev-scale:
|
dev-scale:
|
||||||
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||||
|
|
||||||
stage:
|
stage:
|
||||||
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
pull-stage:
|
||||||
|
docker-compose -f ./docker/docker-compose.staging.yml pull
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich_server_test --remove-orphans
|
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=5 --scale immich-microservices=3 --remove-orphans
|
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
|
api:
|
||||||
|
cd ./server && npm run api:generate
|
||||||
135
README.md
135
README.md
@@ -61,33 +61,30 @@ This project is under heavy development, there will be continuous functions, fea
|
|||||||
| | Mobile | Web |
|
| | Mobile | Web |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| Upload and view videos and photos | Yes | Yes
|
| Upload and view videos and photos | Yes | Yes
|
||||||
| Auto backup when app is opened | Yes | N/A
|
| Auto backup when the app is opened | Yes | N/A
|
||||||
| Selective album(s) for backup | Yes | N/A
|
| Selective album(s) for backup | Yes | N/A
|
||||||
| Download photos and videos to local device | Yes | Yes
|
| Download photos and videos to local device | Yes | Yes
|
||||||
| Multi-user support | Yes | Yes
|
| Multi-user support | Yes | Yes
|
||||||
| Shared Albums | Yes | No
|
| Album | Yes | Yes
|
||||||
|
| Shared Albums | Yes | Yes
|
||||||
| Quick navigation with draggable scrollbar | Yes | Yes
|
| Quick navigation with draggable scrollbar | Yes | Yes
|
||||||
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
|
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
|
||||||
| Metadata view (EXIF, map) | Yes | Yes
|
| Metadata view (EXIF, map) | Yes | Yes
|
||||||
| Search by metadata, objects and image tags | Yes | No
|
| Search by metadata, objects and image tags | Yes | No
|
||||||
| Administrative functions (user management) | No | Yes
|
| Administrative functions (user management) | N/A | Yes
|
||||||
|
|
||||||
|
|
||||||
# System Requirement
|
# System Requirement
|
||||||
|
|
||||||
**OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
|
**OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
|
||||||
|
|
||||||
I haven't tested with `Docker for Windows` as well as `WSL` on Windows
|
|
||||||
|
|
||||||
*Raspberry Pi can be used but `microservices` container has to be comment out in `docker-compose` since TensorFlow has not been supported in Docker image on arm64v7 yet.*
|
|
||||||
|
|
||||||
**RAM**: At least 2GB, preffered 4GB.
|
**RAM**: At least 2GB, preffered 4GB.
|
||||||
|
|
||||||
**Core**: At least 2 cores, preffered 4 cores.
|
**Core**: At least 2 cores, preffered 4 cores.
|
||||||
|
|
||||||
# Getting Started
|
# Technology Stack
|
||||||
|
|
||||||
You can use docker compose for development and testing out the application, there are several services that compose Immich:
|
There are several services that compose Immich:
|
||||||
|
|
||||||
1. **NestJs** - Backend of the application
|
1. **NestJs** - Backend of the application
|
||||||
2. **SvelteKit** - Web frontend of the application
|
2. **SvelteKit** - Web frontend of the application
|
||||||
@@ -96,19 +93,51 @@ You can use docker compose for development and testing out the application, ther
|
|||||||
5. **Nginx** - Load balancing and optimized file uploading.
|
5. **Nginx** - Load balancing and optimized file uploading.
|
||||||
6. **TensorFlow** - Object Detection (COCO SSD) and Image Classification (ImageNet).
|
6. **TensorFlow** - Object Detection (COCO SSD) and Image Classification (ImageNet).
|
||||||
|
|
||||||
## Step 1: Populate .env file
|
# Installing
|
||||||
|
|
||||||
Navigate to `docker` directory and run
|
## One-step installation - for evaluating only
|
||||||
|
|
||||||
```
|
*Applicable system: Ubuntu, Debian, MacOS*
|
||||||
cp .env.example .env
|
|
||||||
|
*This installation method is for evaluating Immich before futher customization to meet the users' needs.*
|
||||||
|
|
||||||
|
In the shell, from the directory of your choice, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -o- https://raw.githubusercontent.com/immich-app/immich/main/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Then populate the value in there.
|
This script will download the `docker-compose.yml` file and the `.env` file, then populate the necessary information, and finally run the `docker-compose up` or `docker compose up` (based on your docker's version) command.
|
||||||
|
|
||||||
Notice that if set `ENABLE_MAPBOX` to `true`, you will have to provide `MAPBOX_KEY` for the server to run.
|
The web application will be available at `http://<machine-ip-address>:2283`, and the server URL for the mobile app will be `http://<machine-ip-address>:2283/api`.
|
||||||
|
|
||||||
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
|
The directory which is used to store the backup file is `./immich-app/immich-data`.
|
||||||
|
|
||||||
|
|
||||||
|
## Customize installation - for production usage
|
||||||
|
|
||||||
|
### Step 1 - Download necessary files
|
||||||
|
|
||||||
|
Create a directory called `immich-app` and cd into it. Then
|
||||||
|
|
||||||
|
Get `docker-compose.yml`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Get `.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget -O .env wget https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 - Populate .env file with customed information
|
||||||
|
|
||||||
|
* Populate customised database information if necessary.
|
||||||
|
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
|
||||||
|
* Populate a secret value for `JWT_SECRET`
|
||||||
|
* [Optional] Populate Mapbox value.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
@@ -136,42 +165,15 @@ JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
|||||||
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
ENABLE_MAPBOX=false
|
ENABLE_MAPBOX=false
|
||||||
MAPBOX_KEY=
|
MAPBOX_KEY=
|
||||||
|
|
||||||
###################################################################################
|
|
||||||
# WEB
|
|
||||||
###################################################################################
|
|
||||||
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
|
||||||
# know where can it make the request to.
|
|
||||||
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
|
||||||
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
|
|
||||||
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283/api
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 2: Start the server
|
### Step 3 - Start the containers
|
||||||
|
|
||||||
To **start**, run
|
Run `docker-compose up` or `docker compose up` (based on your docker's version)
|
||||||
|
|
||||||
```bash
|
### Step 4 - Register admin user
|
||||||
docker-compose -f ./docker/docker-compose.yml up
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have a few thousand photos/videos, I suggest running docker-compose with *scaling* option for the `immich_server` container to handle high I/O load when using fast scrolling.
|
Navigate to the web at `http://<machine-ip-address>:2283` and follow the prompts to register admin user.
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f ./docker/docker-compose.yml up --scale immich-server=5
|
|
||||||
```
|
|
||||||
|
|
||||||
To *update* docker-compose with newest image (if you have started the docker-compose previously)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
|
||||||
```
|
|
||||||
|
|
||||||
The server will be running at `http://your-ip:2283/api` through `Nginx`
|
|
||||||
|
|
||||||
## Step 3: Register User
|
|
||||||
|
|
||||||
Access the web interface at `http://your-ip:2283` to register an admin account.
|
|
||||||
|
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
||||||
@@ -183,14 +185,16 @@ Additional accounts on the server can be created by the admin account.
|
|||||||
<img src="design/admin-interface.png" width="500" title="Admin User Management">
|
<img src="design/admin-interface.png" width="500" title="Admin User Management">
|
||||||
<p/>
|
<p/>
|
||||||
|
|
||||||
## Step 4: Run mobile app
|
### Step 5 - Access the mobile app
|
||||||
|
|
||||||
Login the mobile app with your server address
|
Login the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283/api`
|
||||||
|
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/login-screen.jpeg" width="250" title="Example login screen">
|
<img src="design/login-screen.jpeg" width="250" title="Example login screen">
|
||||||
<p/>
|
<p/>
|
||||||
|
|
||||||
|
## Mobile app
|
||||||
|
|
||||||
## F-Droid
|
## F-Droid
|
||||||
You can get the app on F-droid by clicking the image below.
|
You can get the app on F-droid by clicking the image below.
|
||||||
|
|
||||||
@@ -231,11 +235,34 @@ make dev # required Makefile installed on the system.
|
|||||||
|
|
||||||
All servers and web container are hot reload for quick feedback loop.
|
All servers and web container are hot reload for quick feedback loop.
|
||||||
|
|
||||||
|
## Note for developers
|
||||||
|
### 1 - OpenAPI
|
||||||
|
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). When you add a new or modify an existing endpoint, you must run the generate command below to update the client SDK.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run api:generate # Run from server directory
|
||||||
|
```
|
||||||
|
You can find the generated client SDK in the [`web/src/api`](web/src/api) for Typescript SDK and [`mobile/openapi`](mobile/openapi) for Dart SDK.
|
||||||
|
|
||||||
# Support
|
# Support
|
||||||
|
|
||||||
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below.
|
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**one time**](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or monthly donation from [**Github Sponsor**](https://github.com/sponsors/alextran1502)
|
||||||
|
|
||||||
|
You can also donate using crypto currency with the following addresses:
|
||||||
|
|
||||||
|
<p align="left" style="display: flex; place-items: center; gap: 20px" title="Bitcoin(BTC)">
|
||||||
|
<img src="design/bitcoin.png" width="25" title="Bitcoin">
|
||||||
|
<code>1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p align="left" style="display: flex; place-items: center; gap: 15px" title="Cardano(ADA)">
|
||||||
|
<img src="design/cardano.png" width="30" title="Cardano">
|
||||||
|
<code>
|
||||||
|
addr1qyy567vqhqrr3p7vpszr5p264gw89sqcwts2z8wqy4yek87cdmy79zazyjp7tmwhkluhk3krvslkzfvg0h43tytp3f5q49nycc
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
[](https://www.buymeacoffee.com/altran1502)
|
|
||||||
|
|
||||||
This is also a meaningful way to give me motivation and encouragement to continue working on the app.
|
This is also a meaningful way to give me motivation and encouragement to continue working on the app.
|
||||||
|
|
||||||
@@ -245,7 +272,7 @@ Cheers! 🎉
|
|||||||
|
|
||||||
## TensorFlow Build Issue
|
## TensorFlow Build Issue
|
||||||
|
|
||||||
*This is a known issue on RaspberryPi 4 arm64-v7 and incorrect Promox setup*
|
*This is a known issue for incorrect Promox setup*
|
||||||
|
|
||||||
TensorFlow doesn't run with older CPU architecture, it requires a CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`:
|
TensorFlow doesn't run with older CPU architecture, it requires a CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`:
|
||||||
|
|
||||||
@@ -258,7 +285,3 @@ If you are running virtualization in Promox, the VM doesn't have the flag enable
|
|||||||
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
|
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
|
||||||
|
|
||||||
`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host`
|
`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host`
|
||||||
|
|
||||||
Otherwise you can:
|
|
||||||
- edit `docker-compose.yml` file and comment the whole `immich-machine-learning` service **which will disable machine learning features like object detection and image classification**
|
|
||||||
- switch to a different VM/desktop with different architecture.
|
|
||||||
|
|||||||
BIN
design/bitcoin.png
Normal file
BIN
design/bitcoin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
design/cardano.png
Normal file
BIN
design/cardano.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -7,6 +7,8 @@ DB_USERNAME=postgres
|
|||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
DB_DATABASE_NAME=immich
|
DB_DATABASE_NAME=immich
|
||||||
|
|
||||||
|
# Optional Database settings:
|
||||||
|
# DB_PORT=5432
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -17,6 +19,12 @@ DB_DATABASE_NAME=immich
|
|||||||
|
|
||||||
REDIS_HOSTNAME=immich_redis
|
REDIS_HOSTNAME=immich_redis
|
||||||
|
|
||||||
|
# Optional Redis settings:
|
||||||
|
# REDIS_PORT=6379
|
||||||
|
# REDIS_DBINDEX=0
|
||||||
|
# REDIS_PASSWORD=
|
||||||
|
# REDIS_SOCKET=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -48,16 +56,11 @@ ENABLE_MAPBOX=false
|
|||||||
MAPBOX_KEY=
|
MAPBOX_KEY=
|
||||||
|
|
||||||
|
|
||||||
|
####################################################################################
|
||||||
|
# WEB - Optional
|
||||||
|
####################################################################################
|
||||||
|
|
||||||
|
# Custom message on the login page, should be written in HTML form.
|
||||||
|
# For example VITE_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
||||||
|
|
||||||
###################################################################################
|
VITE_LOGIN_PAGE_MESSAGE=
|
||||||
# WEB
|
|
||||||
###################################################################################
|
|
||||||
|
|
||||||
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
|
||||||
# know where can it make the request to.
|
|
||||||
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
|
||||||
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
|
|
||||||
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
|
|
||||||
|
|
||||||
VITE_SERVER_ENDPOINT=
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Database
|
# Database
|
||||||
DB_HOSTNAME=immich_postgres_test
|
DB_HOSTNAME=immich-database-test
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
DB_DATABASE_NAME=e2e_test
|
DB_DATABASE_NAME=e2e_test
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ services:
|
|||||||
- ../web:/usr/src/app
|
- ../web:/usr/src/app
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
restart: always
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- immich-server
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server_test:
|
immich-server-test:
|
||||||
image: immich-server-dev:latest
|
image: immich-server-test
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -17,15 +17,17 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- immich-redis-test
|
||||||
- database
|
- immich-database-test
|
||||||
|
networks:
|
||||||
redis:
|
- immich-test-network
|
||||||
container_name: immich_redis_test
|
immich-redis-test:
|
||||||
|
container_name: immich-redis-test
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
|
networks:
|
||||||
database:
|
- immich-test-network
|
||||||
container_name: immich_postgres_test
|
immich-database-test:
|
||||||
|
container_name: immich-database-test
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
env_file:
|
env_file:
|
||||||
- .env.test
|
- .env.test
|
||||||
@@ -36,5 +38,8 @@ services:
|
|||||||
PG_DATA: /var/lib/postgresql/data
|
PG_DATA: /var/lib/postgresql/data
|
||||||
volumes:
|
volumes:
|
||||||
- /var/lib/postgresql/data
|
- /var/lib/postgresql/data
|
||||||
ports:
|
networks:
|
||||||
- 5432:5432
|
- immich-test-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
immich-test-network:
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ services:
|
|||||||
image: altran1502/immich-proxy:release
|
image: altran1502/immich-proxy:release
|
||||||
ports:
|
ports:
|
||||||
- 2283:80
|
- 2283:80
|
||||||
- 2284:443
|
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
83
install.sh
Executable file
83
install.sh
Executable file
@@ -0,0 +1,83 @@
|
|||||||
|
echo "Starting Immich installation..."
|
||||||
|
|
||||||
|
ip_address=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\032[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
machine_has() {
|
||||||
|
type "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
create_immich_directory() {
|
||||||
|
echo "Creating Immich directory..."
|
||||||
|
mkdir -p ./immich-app/immich-data
|
||||||
|
}
|
||||||
|
|
||||||
|
download_docker_compose_file() {
|
||||||
|
echo "Downloading docker-compose.yml..."
|
||||||
|
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
download_dot_env_file() {
|
||||||
|
echo "Downloading .env file..."
|
||||||
|
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
populate_upload_location() {
|
||||||
|
echo "Populating default UPLOAD_LOCATION value..."
|
||||||
|
|
||||||
|
cd ./immich-app/immich-data
|
||||||
|
|
||||||
|
upload_location=$(pwd)
|
||||||
|
|
||||||
|
# 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() {
|
||||||
|
echo "Starting Immich's docker containers"
|
||||||
|
|
||||||
|
if machine_has "docker compose"; then {
|
||||||
|
docker compose up --remove-orphans -d
|
||||||
|
|
||||||
|
show_friendly_message
|
||||||
|
exit 0
|
||||||
|
}; fi
|
||||||
|
|
||||||
|
if machine_has "docker-compose"; then
|
||||||
|
docker-compose up --remove-orphans -d
|
||||||
|
|
||||||
|
show_friendly_message
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_friendly_message() {
|
||||||
|
echo "Succesfully deployed Immich!"
|
||||||
|
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
|
||||||
|
echo "The backup (or upload) location is $upload_location"
|
||||||
|
echo "---------------------------------------------------"
|
||||||
|
echo "If you want to confgure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
|
||||||
|
|
||||||
|
1. First bring down the containers with the command 'docker-compose down' in the immich-app directory,
|
||||||
|
|
||||||
|
2. Then change the information that fits your needs in the '.env' file,
|
||||||
|
|
||||||
|
3. Finally, bring the containers back up with the command 'docker-compose up --remove-orphans -d' in the immich-app directory"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# MAIN
|
||||||
|
create_immich_directory
|
||||||
|
download_docker_compose_file
|
||||||
|
download_dot_env_file
|
||||||
|
populate_upload_location
|
||||||
|
start_docker_compose
|
||||||
19
localizely.yml
Normal file
19
localizely.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
config_version: 1.0
|
||||||
|
project_id: ead34689-ec52-41d9-b675-09bc85a6cbd7
|
||||||
|
file_type: json
|
||||||
|
upload:
|
||||||
|
files:
|
||||||
|
- file: mobile/assets/i18n/en-US.json
|
||||||
|
locale_code: en-US
|
||||||
|
- file: mobile/assets/i18n/de-DE.json
|
||||||
|
locale_code: de-DE
|
||||||
|
- file: mobile/assets/i18n/fr-FR.json
|
||||||
|
locale_code: fr-FR
|
||||||
|
download:
|
||||||
|
files:
|
||||||
|
- file: mobile/assets/i18n/en-US.json
|
||||||
|
locale_code: en-US
|
||||||
|
- file: mobile/assets/i18n/de-DE.json
|
||||||
|
locale_code: de-DE
|
||||||
|
- file: mobile/assets/i18n/fr-FR.json
|
||||||
|
locale_code: fr-FR
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:16-bullseye-slim
|
FROM node:16-bullseye-slim
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
@@ -9,7 +9,8 @@ COPY package.json package-lock.json ./
|
|||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
||||||
|
|
||||||
RUN npm install
|
RUN npm ci
|
||||||
|
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|||||||
667
machine-learning/package-lock.json
generated
667
machine-learning/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,11 +28,11 @@
|
|||||||
"@nestjs/typeorm": "^8.0.3",
|
"@nestjs/typeorm": "^8.0.3",
|
||||||
"@tensorflow-models/coco-ssd": "^2.2.2",
|
"@tensorflow-models/coco-ssd": "^2.2.2",
|
||||||
"@tensorflow-models/mobilenet": "^2.1.0",
|
"@tensorflow-models/mobilenet": "^2.1.0",
|
||||||
"@tensorflow/tfjs": "^3.15.0",
|
"@tensorflow/tfjs": "^3.19.0",
|
||||||
"@tensorflow/tfjs-converter": "^3.15.0",
|
"@tensorflow/tfjs-converter": "^3.19.0",
|
||||||
"@tensorflow/tfjs-core": "^3.15.0",
|
"@tensorflow/tfjs-core": "^3.19.0",
|
||||||
"@tensorflow/tfjs-node": "^3.15.0",
|
"@tensorflow/tfjs-node": "^3.19.0",
|
||||||
"@tensorflow/tfjs-node-gpu": "^3.15.0",
|
"@tensorflow/tfjs-node-gpu": "^3.19.0",
|
||||||
"@trpc/server": "^9.20.3",
|
"@trpc/server": "^9.20.3",
|
||||||
"pg": "^8.7.3",
|
"pg": "^8.7.3",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|||||||
export const databaseConfig: TypeOrmModuleOptions = {
|
export const databaseConfig: TypeOrmModuleOptions = {
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: process.env.DB_HOSTNAME || 'immich_postgres',
|
host: process.env.DB_HOSTNAME || 'immich_postgres',
|
||||||
port: 5432,
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
username: process.env.DB_USERNAME,
|
username: process.env.DB_USERNAME,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.DB_DATABASE_NAME,
|
database: process.env.DB_DATABASE_NAME,
|
||||||
|
|||||||
2
mobile/.gitignore
vendored
2
mobile/.gitignore
vendored
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
# Flutter/Dart/Pub related
|
# Flutter/Dart/Pub related
|
||||||
**/doc/api/
|
**/doc/api/
|
||||||
**/ios/Flutter/.last_build_id
|
**/ios/
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.flutter-plugins
|
.flutter-plugins
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
|
|||||||
@@ -21,10 +21,18 @@ linter:
|
|||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
# producing the lint.
|
# producing the lint.
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
use_build_context_synchronously: false
|
use_build_context_synchronously: false
|
||||||
|
require_trailing_commas: true
|
||||||
|
unrelated_type_equality_checks: true
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- openapi/
|
||||||
|
- openapi/test/
|
||||||
|
- lib/generated_plugin_registrant.dart
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
json_key_file("/Users/alex/Documents/immich-fastlane-googleplaystore-key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
|
json_key_file("/Users/alex/Documents/immich-play-store-key.json")
|
||||||
package_name("app.alextran.immich") # e.g. com.krausefx.app
|
package_name("app.alextran.immich")
|
||||||
|
|||||||
@@ -16,10 +16,25 @@
|
|||||||
default_platform(:android)
|
default_platform(:android)
|
||||||
|
|
||||||
platform :android do
|
platform :android do
|
||||||
|
desc "Build Android"
|
||||||
|
lane :build do
|
||||||
|
gradle(
|
||||||
|
task: 'bundle',
|
||||||
|
build_type: 'Release',
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
desc "Update AAB to PlayStore"
|
desc "Build and Release Android"
|
||||||
lane :beta do
|
lane :release do
|
||||||
upload_to_play_store(track: 'beta', aab: '../build/app/outputs/bundle/release/app-release.aab')
|
gradle(
|
||||||
|
task: 'bundle',
|
||||||
|
build_type: 'Release',
|
||||||
|
properties: {
|
||||||
|
"android.injected.version.code" => 30,
|
||||||
|
"android.injected.version.name" => "1.20.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')
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
|
|||||||
|
|
||||||
## Android
|
## Android
|
||||||
|
|
||||||
### android beta
|
### android release
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
[bundle exec] fastlane android beta
|
[bundle exec] fastlane android release
|
||||||
```
|
```
|
||||||
|
|
||||||
Update AAB to PlayStore
|
Update AAB to PlayStore
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
* Hot fix: Restore shared album functionality
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Add information for uploading asset and error indication with error message for each failed upload.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Refactored app to use OpenAPI SDK to improve performance and project structure.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Refactored app to use OpenAPI SDK to improve performance and project structure.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Added other languages to app
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Added French, Danish, Spanish, French, Japanese, Polish, and Finish translation to the app
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* New feature - Gallery view now enable with swipping action
|
||||||
|
* New feature - Add album feature
|
||||||
@@ -5,14 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000318">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000204">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: upload_to_play_store" time="111.253169">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="11.673502">
|
||||||
|
|
||||||
<failure message="/usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:229:in `chdir' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:22:in `block (2 levels) in parsing_binding' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/lane.rb:33:in `call' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:45:in `chdir' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/runner.rb:45:in `execute' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/commands_generator.rb:109:in `block (2 levels) in run' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/commands_generator.rb:353:in `run' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/commands_generator.rb:42:in `start' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/fastlane/lib/fastlane/cli_tools_distributor.rb:122:in `take_off' /usr/local/Cellar/fastlane/2.204.3/libexec/gems/fastlane-2.204.3/bin/fastlane:23:in `<top (required)>' /usr/local/Cellar/fastlane/2.204.3/libexec/bin/fastlane:25:in `load' /usr/local/Cellar/fastlane/2.204.3/libexec/bin/fastlane:25:in `<main>' Google Api Error: Invalid request - APK specifies a version code that has already been used." />
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="37.162935">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
106
mobile/assets/i18n/da-DK.json
Normal file
106
mobile/assets/i18n/da-DK.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "EKSKLUDERET",
|
||||||
|
"album_info_card_backup_album_included": "INKLUDERET",
|
||||||
|
"album_viewer_appbar_share_delete": "Slet album",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Fejlede sletning af album",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Fejlede i at forlade album",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Der er problemer med at fjerne elementer fra album",
|
||||||
|
"album_viewer_appbar_share_err_title": "Fejlede i at ændre albumtitel",
|
||||||
|
"album_viewer_appbar_share_leave": "Forlad album",
|
||||||
|
"album_viewer_appbar_share_remove": "Fjern fra album",
|
||||||
|
"album_viewer_page_share_add_users": "Tilføj brugere",
|
||||||
|
"backup_album_selection_page_albums_device": "Albummer på enhed ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Tryk en gang for at inkludere, tryk to gange for at ekskludere",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Elementer kan være spredt på tværs af flere albummer. Albummer kan således inkluderes eller udelukkes under sikkerhedskopieringsprocessen.",
|
||||||
|
"backup_album_selection_page_select_albums": "Vælg albummer",
|
||||||
|
"backup_album_selection_page_selection_info": "Oplysninger om valgte",
|
||||||
|
"backup_album_selection_page_total_assets": "Samlede unikke elementer",
|
||||||
|
"backup_all": "Alt",
|
||||||
|
"backup_controller_page_albums": "Sikkerhedskopier albummer",
|
||||||
|
"backup_controller_page_backup": "Sikkerhedskopier",
|
||||||
|
"backup_controller_page_backup_selected": "Valgte: ",
|
||||||
|
"backup_controller_page_backup_sub": "Sikkerhedskopierede billeder og videoer",
|
||||||
|
"backup_controller_page_cancel": "Annuller",
|
||||||
|
"backup_controller_page_created": "Oprettet den: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Slå sikkerhedskopiering til automatisk at uploade nye elementer til serveren.",
|
||||||
|
"backup_controller_page_excluded": "Ekskluderet: ",
|
||||||
|
"backup_controller_page_failed": "Felet ({})",
|
||||||
|
"backup_controller_page_filename": "Filnavn: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Sikkerhedskopieringsinformation",
|
||||||
|
"backup_controller_page_none_selected": "Ingen valgte",
|
||||||
|
"backup_controller_page_remainder": "Tilbageværende",
|
||||||
|
"backup_controller_page_remainder_sub": "Tilbageværende billeder og albummer, at sikkerhedskopiere, fra valgte",
|
||||||
|
"backup_controller_page_select": "Vælg",
|
||||||
|
"backup_controller_page_server_storage": "Serverlager",
|
||||||
|
"backup_controller_page_start_backup": "Start sikkerhedskopiering",
|
||||||
|
"backup_controller_page_status_off": "Sikkerhedskopiering er slået fra",
|
||||||
|
"backup_controller_page_status_on": "Sikkerhedskopiering er slået til",
|
||||||
|
"backup_controller_page_storage_format": "{} af {} brugt",
|
||||||
|
"backup_controller_page_to_backup": "Albummer at sikkerhedskopiere",
|
||||||
|
"backup_controller_page_total": "I alt",
|
||||||
|
"backup_controller_page_total_sub": "Alle unikke billeder og videoer fra valgte albummer",
|
||||||
|
"backup_controller_page_turn_off": "Slå sikkerhedskopiering fra",
|
||||||
|
"backup_controller_page_turn_on": "Slå sikkerhedskopiering til",
|
||||||
|
"backup_controller_page_uploading_file_info": "Uploader filinformation",
|
||||||
|
"backup_err_only_album": "Kan ikke slette det eneste album",
|
||||||
|
"backup_info_card_assets": "elementer",
|
||||||
|
"control_bottom_app_bar_delete": "Slet",
|
||||||
|
"create_shared_album_page_share": "Del",
|
||||||
|
"create_shared_album_page_share_add_assets": "TILFØJ ELEMENT",
|
||||||
|
"create_shared_album_page_share_select_photos": "Vælg billeder",
|
||||||
|
"daily_title_text_date": "E, dd MMM",
|
||||||
|
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||||
|
"date_format": "E d. LLL y • hh:mm",
|
||||||
|
"delete_dialog_alert": "Disse elementer vil blive slettet permanent fra Immich og din enhed",
|
||||||
|
"delete_dialog_cancel": "Annuller",
|
||||||
|
"delete_dialog_ok": "Slet",
|
||||||
|
"delete_dialog_title": "Slet permanent",
|
||||||
|
"exif_bottom_sheet_description": "Tilføj beskrivelse...",
|
||||||
|
"exif_bottom_sheet_details": "DETALJER",
|
||||||
|
"exif_bottom_sheet_location": "LOKATION",
|
||||||
|
"login_form_button_text": "Log ind",
|
||||||
|
"login_form_email_hint": "din-email@email.com",
|
||||||
|
"login_form_endpoint_hint": "http://din-server-ip:port/api",
|
||||||
|
"login_form_endpoint_url": "Server Endpoint URL",
|
||||||
|
"login_form_err_http": "Angiv venligst http:// eller https://",
|
||||||
|
"login_form_err_invalid_email": "Ugyldig email",
|
||||||
|
"login_form_err_leading_whitespace": "Mellemrum før",
|
||||||
|
"login_form_err_trailing_whitespace": "Mellemrum efter",
|
||||||
|
"login_form_failed_login": "Der opstod en vejl ved at logge ind. Tjek server URL, email og kodeordet",
|
||||||
|
"login_form_label_email": "Email",
|
||||||
|
"login_form_label_password": "Kodeord",
|
||||||
|
"login_form_password_hint": "kodeord",
|
||||||
|
"login_form_save_login": "Forbliv logget ind",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "Klient og server er ajour",
|
||||||
|
"profile_drawer_sign_out": "Log ud",
|
||||||
|
"search_bar_hint": "Søg i dine billeder",
|
||||||
|
"search_page_no_objects": "Ingen elementer er tilgængelige",
|
||||||
|
"search_page_no_places": "Ingen placeringsinformation er tilgængelig",
|
||||||
|
"search_page_places": "Steder",
|
||||||
|
"search_page_things": "Ting",
|
||||||
|
"search_result_page_new_search_hint": "Ny søgning",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Anbefalinger",
|
||||||
|
"select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Anbefalinger",
|
||||||
|
"share_add": "Tilføj",
|
||||||
|
"share_add_photos": "Tilføj billeder",
|
||||||
|
"share_add_title": "Tilføj en titel",
|
||||||
|
"share_create_album": "Opret album",
|
||||||
|
"share_invite": "Inviter til album",
|
||||||
|
"sharing_page_album": "Delt albums",
|
||||||
|
"sharing_page_description": "Opret delte albummer for at dele billeder og video med personer på dit netværk.",
|
||||||
|
"sharing_page_empty_list": "TOM LISTE",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Opret delt album",
|
||||||
|
"sharing_silver_appbar_share_partner": "Del med partner",
|
||||||
|
"tab_controller_nav_photos": "Billeder",
|
||||||
|
"tab_controller_nav_search": "Søg",
|
||||||
|
"tab_controller_nav_sharing": "Deling",
|
||||||
|
"version_announcement_overlay_ack": "Vedkend",
|
||||||
|
"version_announcement_overlay_release_notes": "udgivelsesnoter",
|
||||||
|
"version_announcement_overlay_text_1": "Hej vej, der er en ny version af",
|
||||||
|
"version_announcement_overlay_text_2": "bresøg venligst ",
|
||||||
|
"version_announcement_overlay_text_3": " og sikker dig, at din dockercompose og .env-fil er opdateret, for at undgå fejlkonfiguration, specielt hvis u bruger WatchTowereller andre mekanisme, der automatisk opdaterer serverprogrammer.",
|
||||||
|
"version_announcement_overlay_title": "Ny serverversion er tilgængelig \uD83C\uDF89"
|
||||||
|
}
|
||||||
106
mobile/assets/i18n/de-DE.json
Normal file
106
mobile/assets/i18n/de-DE.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
|
||||||
|
"album_info_card_backup_album_included": "EINGESCHLOSSEN",
|
||||||
|
"album_viewer_appbar_share_delete": "Album löschen",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Beim Löschen von Elementen aus dem Album ist ein Problem aufgetreten",
|
||||||
|
"album_viewer_appbar_share_err_title": "Der Titel konnte nicht geändert werden",
|
||||||
|
"album_viewer_appbar_share_leave": "Album verlassen",
|
||||||
|
"album_viewer_appbar_share_remove": "Entferne vom Album",
|
||||||
|
"album_viewer_page_share_add_users": "Nutzer hinzufügen",
|
||||||
|
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
|
||||||
|
"backup_album_selection_page_select_albums": "Alben auswählen",
|
||||||
|
"backup_album_selection_page_selection_info": "Auswahl",
|
||||||
|
"backup_album_selection_page_total_assets": "Elemente",
|
||||||
|
"backup_all": "Alle",
|
||||||
|
"backup_controller_page_albums": "Gesicherte Alben",
|
||||||
|
"backup_controller_page_backup": "Sicherung",
|
||||||
|
"backup_controller_page_backup_selected": "Ausgewählt: ",
|
||||||
|
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
|
||||||
|
"backup_controller_page_cancel": "Abbrechen",
|
||||||
|
"backup_controller_page_created": "Erstellt: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.",
|
||||||
|
"backup_controller_page_excluded": "Ausgeschlossen: ",
|
||||||
|
"backup_controller_page_failed": "Fehlgeschlagen ({})",
|
||||||
|
"backup_controller_page_filename": "Dateiname: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Informationen zur Sicherung",
|
||||||
|
"backup_controller_page_none_selected": "Keine ausgewählt",
|
||||||
|
"backup_controller_page_remainder": "Übrig",
|
||||||
|
"backup_controller_page_remainder_sub": "Noch zu sichernde Fotos und Videos",
|
||||||
|
"backup_controller_page_select": "Auswählen",
|
||||||
|
"backup_controller_page_server_storage": "Server Speicher",
|
||||||
|
"backup_controller_page_start_backup": "Sicherung starten",
|
||||||
|
"backup_controller_page_status_off": "Sicherung ist inaktiv",
|
||||||
|
"backup_controller_page_status_on": "Sicherung ist aktiv",
|
||||||
|
"backup_controller_page_storage_format": "{} von {} genutzt",
|
||||||
|
"backup_controller_page_to_backup": "Zu sichernde Alben",
|
||||||
|
"backup_controller_page_total": "Gesamt",
|
||||||
|
"backup_controller_page_total_sub": "Alle Fotos und Videos",
|
||||||
|
"backup_controller_page_turn_off": "Sicherung ausschalten",
|
||||||
|
"backup_controller_page_turn_on": "Sicherung einschalten",
|
||||||
|
"backup_controller_page_uploading_file_info": "Informationen",
|
||||||
|
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
|
||||||
|
"backup_info_card_assets": "Elemente",
|
||||||
|
"control_bottom_app_bar_delete": "Löschen",
|
||||||
|
"create_shared_album_page_share": "Teilen",
|
||||||
|
"create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
|
||||||
|
"create_shared_album_page_share_select_photos": "Fotos auswählen",
|
||||||
|
"daily_title_text_date": "E, dd MMM",
|
||||||
|
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||||
|
"date_format": "E d. LLL y • hh:mm",
|
||||||
|
"delete_dialog_alert": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt",
|
||||||
|
"delete_dialog_cancel": "Abbrechen",
|
||||||
|
"delete_dialog_ok": "Löschen",
|
||||||
|
"delete_dialog_title": "Für immer löschen",
|
||||||
|
"exif_bottom_sheet_description": "Beschreibung hinzufügen...",
|
||||||
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
|
"exif_bottom_sheet_location": "STANDORT",
|
||||||
|
"login_form_button_text": "Anmelden",
|
||||||
|
"login_form_email_hint": "deine@email.de",
|
||||||
|
"login_form_endpoint_hint": "http://deine-server-ip:port/api",
|
||||||
|
"login_form_endpoint_url": "Server URL",
|
||||||
|
"login_form_err_http": "Bitte gebe http:// oder https:// an",
|
||||||
|
"login_form_err_invalid_email": "Ungültige E-Mail",
|
||||||
|
"login_form_err_leading_whitespace": "Führendes Leerzichen",
|
||||||
|
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
|
||||||
|
"login_form_failed_login": "Fehler bei der Anmeldung, überprüfen Sie Server URL, E-Mail und Passwort",
|
||||||
|
"login_form_label_email": "E-Mail",
|
||||||
|
"login_form_label_password": "Passwort",
|
||||||
|
"login_form_password_hint": "Passwort",
|
||||||
|
"login_form_save_login": "Angemeldet bleiben",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
|
||||||
|
"profile_drawer_sign_out": "Abmelden",
|
||||||
|
"search_bar_hint": "Durchsuche deine Fotos",
|
||||||
|
"search_page_no_objects": "Keine Objektinformationen verfügbar",
|
||||||
|
"search_page_no_places": "Keine Informationen über Orte verfügbar",
|
||||||
|
"search_page_places": "Orte",
|
||||||
|
"search_page_things": "Dinge",
|
||||||
|
"search_result_page_new_search_hint": "Neue Suche",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
|
||||||
|
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Vorschläge",
|
||||||
|
"share_add": "Hinzufügen",
|
||||||
|
"share_add_photos": "Fotos hinzufügen",
|
||||||
|
"share_add_title": "Titel hinzufügen",
|
||||||
|
"share_create_album": "Album erstellen",
|
||||||
|
"share_invite": "Zum Album einladen",
|
||||||
|
"sharing_page_album": "Geteilte Alben",
|
||||||
|
"sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.",
|
||||||
|
"sharing_page_empty_list": "LEERE LISTE",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Neues geteiltes Album",
|
||||||
|
"sharing_silver_appbar_share_partner": "Teile mit Partner",
|
||||||
|
"tab_controller_nav_photos": "Fotos",
|
||||||
|
"tab_controller_nav_search": "Suche",
|
||||||
|
"tab_controller_nav_sharing": "Teilen",
|
||||||
|
"version_announcement_overlay_ack": "Ich habe verstanden",
|
||||||
|
"version_announcement_overlay_release_notes": "Änderungsprotokoll",
|
||||||
|
"version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
|
||||||
|
"version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ",
|
||||||
|
"version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
|
||||||
|
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89"
|
||||||
|
}
|
||||||
108
mobile/assets/i18n/en-US.json
Normal file
108
mobile/assets/i18n/en-US.json
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||||
|
"album_info_card_backup_album_included": "INCLUDED",
|
||||||
|
"album_viewer_appbar_share_delete": "Delete album",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Failed to delete album",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Failed to leave album",
|
||||||
|
"album_viewer_appbar_share_err_remove": "There are problems in removing assets from album",
|
||||||
|
"album_viewer_appbar_share_err_title": "Failed to change album title",
|
||||||
|
"album_viewer_appbar_share_leave": "Leave album",
|
||||||
|
"album_viewer_appbar_share_remove": "Remove from album",
|
||||||
|
"album_viewer_page_share_add_users": "Add users",
|
||||||
|
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||||
|
"backup_album_selection_page_select_albums": "Select Albums",
|
||||||
|
"backup_album_selection_page_selection_info": "Selection Info",
|
||||||
|
"backup_album_selection_page_total_assets": "Total unique assets",
|
||||||
|
"backup_all": "All",
|
||||||
|
"backup_controller_page_albums": "Backup Albums",
|
||||||
|
"backup_controller_page_backup": "Backup",
|
||||||
|
"backup_controller_page_backup_selected": "Selected: ",
|
||||||
|
"backup_controller_page_backup_sub": "Backed up photos and videos",
|
||||||
|
"backup_controller_page_cancel": "Cancel",
|
||||||
|
"backup_controller_page_created": "Created on: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Turn on backup to automatically upload new assets to the server.",
|
||||||
|
"backup_controller_page_excluded": "Excluded: ",
|
||||||
|
"backup_controller_page_failed": "Failed ({})",
|
||||||
|
"backup_controller_page_filename": "File name: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Backup Information",
|
||||||
|
"backup_controller_page_none_selected": "None selected",
|
||||||
|
"backup_controller_page_remainder": "Remainder",
|
||||||
|
"backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
|
||||||
|
"backup_controller_page_select": "Select",
|
||||||
|
"backup_controller_page_server_storage": "Server Storage",
|
||||||
|
"backup_controller_page_start_backup": "Start Backup",
|
||||||
|
"backup_controller_page_status_off": "Backup is off",
|
||||||
|
"backup_controller_page_status_on": "Backup is on",
|
||||||
|
"backup_controller_page_storage_format": "{} of {} used",
|
||||||
|
"backup_controller_page_to_backup": "Albums to be backup",
|
||||||
|
"backup_controller_page_total": "Total",
|
||||||
|
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
|
||||||
|
"backup_controller_page_turn_off": "Turn off Backup",
|
||||||
|
"backup_controller_page_turn_on": "Turn on Backup",
|
||||||
|
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||||
|
"backup_err_only_album": "Cannot remove the only album",
|
||||||
|
"backup_info_card_assets": "assets",
|
||||||
|
"control_bottom_app_bar_delete": "Delete",
|
||||||
|
"create_shared_album_page_share": "Share",
|
||||||
|
"create_shared_album_page_create": "Create",
|
||||||
|
"create_shared_album_page_share_add_assets": "ADD ASSETS",
|
||||||
|
"create_shared_album_page_share_select_photos": "Select Photos",
|
||||||
|
"daily_title_text_date": "E, MMM dd",
|
||||||
|
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||||
|
"date_format": "E, LLL d, y • h:mm a",
|
||||||
|
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
||||||
|
"delete_dialog_cancel": "Cancel",
|
||||||
|
"delete_dialog_ok": "Delete",
|
||||||
|
"delete_dialog_title": "Delete Permanently",
|
||||||
|
"exif_bottom_sheet_description": "Add Description...",
|
||||||
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
|
"login_form_button_text": "Login",
|
||||||
|
"login_form_email_hint": "youremail@email.com",
|
||||||
|
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||||
|
"login_form_endpoint_url": "Server Endpoint URL",
|
||||||
|
"login_form_err_http": "Please specify http:// or https://",
|
||||||
|
"login_form_err_invalid_email": "Invalid Email",
|
||||||
|
"login_form_err_leading_whitespace": "Leading whitespace",
|
||||||
|
"login_form_err_trailing_whitespace": "Trailing whitespace",
|
||||||
|
"login_form_failed_login": "Error logging you in, check server url, email and password",
|
||||||
|
"login_form_label_email": "Email",
|
||||||
|
"login_form_label_password": "Password",
|
||||||
|
"login_form_password_hint": "password",
|
||||||
|
"login_form_save_login": "Stay logged in",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||||
|
"profile_drawer_sign_out": "Sign Out",
|
||||||
|
"search_bar_hint": "Search your photos",
|
||||||
|
"search_page_no_objects": "No Objects Info Available",
|
||||||
|
"search_page_no_places": "No Places Info Available",
|
||||||
|
"search_page_places": "Places",
|
||||||
|
"search_page_things": "Things",
|
||||||
|
"search_result_page_new_search_hint": "New Search",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
||||||
|
"select_user_for_sharing_page_err_album": "Failed to create album",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||||
|
"share_add": "Add",
|
||||||
|
"share_add_photos": "Add photos",
|
||||||
|
"share_add_title": "Add a title",
|
||||||
|
"share_create_album": "Create album",
|
||||||
|
"share_invite": "Invite to album",
|
||||||
|
"sharing_page_album": "Shared albums",
|
||||||
|
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
|
||||||
|
"sharing_page_empty_list": "EMPTY LIST",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Create shared album",
|
||||||
|
"sharing_silver_appbar_share_partner": "Share with partner",
|
||||||
|
"tab_controller_nav_photos": "Photos",
|
||||||
|
"tab_controller_nav_search": "Search",
|
||||||
|
"tab_controller_nav_sharing": "Sharing",
|
||||||
|
"tab_controller_nav_library": "Library",
|
||||||
|
"version_announcement_overlay_ack": "Acknowledge",
|
||||||
|
"version_announcement_overlay_release_notes": "release notes",
|
||||||
|
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
||||||
|
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
||||||
|
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||||
|
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
|
||||||
|
}
|
||||||
106
mobile/assets/i18n/es-ES.json
Normal file
106
mobile/assets/i18n/es-ES.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "EXCLUIDOS",
|
||||||
|
"album_info_card_backup_album_included": "INCLUIDOS",
|
||||||
|
"album_viewer_appbar_share_delete": "Eliminar álbum ",
|
||||||
|
"album_viewer_appbar_share_err_delete": "No ha podido eliminar el álbum",
|
||||||
|
"album_viewer_appbar_share_err_leave": "No ha podido dejar el álbum",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los activos del álbum",
|
||||||
|
"album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum ",
|
||||||
|
"album_viewer_appbar_share_leave": "Abandonar álbum ",
|
||||||
|
"album_viewer_appbar_share_remove": "Eliminar del álbum ",
|
||||||
|
"album_viewer_page_share_add_users": "Añadir usuarios",
|
||||||
|
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Los activos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
|
||||||
|
"backup_album_selection_page_select_albums": "Seleccionar Álbumes",
|
||||||
|
"backup_album_selection_page_selection_info": "Información sobre la Selección",
|
||||||
|
"backup_album_selection_page_total_assets": "Total de activos únicos",
|
||||||
|
"backup_all": "Todos",
|
||||||
|
"backup_controller_page_albums": "Álbumes de copia de seguridad",
|
||||||
|
"backup_controller_page_backup": "Copia de Seguridad",
|
||||||
|
"backup_controller_page_backup_selected": "Seleccionado:",
|
||||||
|
"backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos",
|
||||||
|
"backup_controller_page_cancel": "Cancelar",
|
||||||
|
"backup_controller_page_created": "",
|
||||||
|
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.",
|
||||||
|
"backup_controller_page_excluded": "Excluido:",
|
||||||
|
"backup_controller_page_failed": "",
|
||||||
|
"backup_controller_page_filename": "",
|
||||||
|
"backup_controller_page_id": "",
|
||||||
|
"backup_controller_page_info": "Información de la Copia de Seguridad",
|
||||||
|
"backup_controller_page_none_selected": "Ninguno seleccionado",
|
||||||
|
"backup_controller_page_remainder": "Remanente",
|
||||||
|
"backup_controller_page_remainder_sub": "Fotos y álbumes restantes para hacer una copia de seguridad de la selección",
|
||||||
|
"backup_controller_page_select": "Seleccionar",
|
||||||
|
"backup_controller_page_server_storage": "Almacenamiento en el servidor",
|
||||||
|
"backup_controller_page_start_backup": "Iniciar copia de seguridad",
|
||||||
|
"backup_controller_page_status_off": "La copia de seguridad está desactivada",
|
||||||
|
"backup_controller_page_status_on": "La copia de seguridad está activada",
|
||||||
|
"backup_controller_page_storage_format": "{} de {} usadas",
|
||||||
|
"backup_controller_page_to_backup": "Álbumes a respaldar",
|
||||||
|
"backup_controller_page_total": "Total",
|
||||||
|
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
|
||||||
|
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
|
||||||
|
"backup_controller_page_turn_on": "Activar la copia de seguridad",
|
||||||
|
"backup_controller_page_uploading_file_info": "",
|
||||||
|
"backup_err_only_album": "No se puede eliminar el único álbum",
|
||||||
|
"backup_info_card_assets": "activos",
|
||||||
|
"control_bottom_app_bar_delete": "Eliminar",
|
||||||
|
"create_shared_album_page_share": "Compartir",
|
||||||
|
"create_shared_album_page_share_add_assets": "AÑADIR ACTIVOS",
|
||||||
|
"create_shared_album_page_share_select_photos": "Seleccionar Fotos",
|
||||||
|
"daily_title_text_date": "E dd, MMM",
|
||||||
|
"daily_title_text_date_year": "E dd de MMM, yyyy",
|
||||||
|
"date_format": "E d, LLL y • h:mm a",
|
||||||
|
"delete_dialog_alert": "Estos elementos serán eliminados permanentemente de Immich y de tu dispositivo",
|
||||||
|
"delete_dialog_cancel": "Cancelar",
|
||||||
|
"delete_dialog_ok": "Eliminar",
|
||||||
|
"delete_dialog_title": "Eliminar Permanentemente",
|
||||||
|
"exif_bottom_sheet_description": "Añadir Descripción...",
|
||||||
|
"exif_bottom_sheet_details": "DETALLES",
|
||||||
|
"exif_bottom_sheet_location": "LOCALZACIÓN",
|
||||||
|
"login_form_button_text": "Iniciar Sesión",
|
||||||
|
"login_form_email_hint": "tucorreo@correo.com",
|
||||||
|
"login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto/api",
|
||||||
|
"login_form_endpoint_url": "URL del servidor",
|
||||||
|
"login_form_err_http": "Por favor, especifique http:// o https://",
|
||||||
|
"login_form_err_invalid_email": "Correo electrónico no válido",
|
||||||
|
"login_form_err_leading_whitespace": "Espacio en blanco inicial",
|
||||||
|
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
|
||||||
|
"login_form_failed_login": "",
|
||||||
|
"login_form_label_email": "Correo",
|
||||||
|
"login_form_label_password": "Contraseña",
|
||||||
|
"login_form_password_hint": "contraseña",
|
||||||
|
"login_form_save_login": "Mantener la sesión iniciada",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
|
||||||
|
"profile_drawer_sign_out": "Cerrar Sesión",
|
||||||
|
"search_bar_hint": "Busca tus fotos",
|
||||||
|
"search_page_no_objects": "",
|
||||||
|
"search_page_no_places": "No hay información de lugares disponibles",
|
||||||
|
"search_page_places": "Lugares",
|
||||||
|
"search_page_things": "Cosas",
|
||||||
|
"search_result_page_new_search_hint": "Nueva Busqueda",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Sugerencias",
|
||||||
|
"select_user_for_sharing_page_err_album": "Fallo al crear el álbum",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "",
|
||||||
|
"share_add": "Añadir",
|
||||||
|
"share_add_photos": "Añadir fotos",
|
||||||
|
"share_add_title": "Añadir un título",
|
||||||
|
"share_create_album": "Crear álbum",
|
||||||
|
"share_invite": "Invitar al álbum",
|
||||||
|
"sharing_page_album": "Álbumes compartidos",
|
||||||
|
"sharing_page_description": "Crea álbumes compartidos para compartir fotos y vídeos con las personas de tu red.",
|
||||||
|
"sharing_page_empty_list": "LISTA VACIA",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Crear un álbum compartido",
|
||||||
|
"sharing_silver_appbar_share_partner": "Compartir con el compañero",
|
||||||
|
"tab_controller_nav_photos": "Fotos",
|
||||||
|
"tab_controller_nav_search": "Buscar",
|
||||||
|
"tab_controller_nav_sharing": "Compartiendo",
|
||||||
|
"version_announcement_overlay_ack": "Reconocer",
|
||||||
|
"version_announcement_overlay_release_notes": "notas de versión",
|
||||||
|
"version_announcement_overlay_text_1": "Hola amigo, hay una nueva versión de",
|
||||||
|
"version_announcement_overlay_text_2": "tómese su tiempo para visitar la ",
|
||||||
|
"version_announcement_overlay_text_3": "y asegurate de que tu configuración de docker-compose y .env está actualizada para evitar cualquier desconfiguración, especialmente si utiliza WatchTower o cualquier mecanismo que se encargue de actualizar su aplicación de servidor automáticamente.",
|
||||||
|
"version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89"
|
||||||
|
}
|
||||||
106
mobile/assets/i18n/fi-FI.json
Normal file
106
mobile/assets/i18n/fi-FI.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "JÄTETTY POIS",
|
||||||
|
"album_info_card_backup_album_included": "SISÄLLYTETTY",
|
||||||
|
"album_viewer_appbar_share_delete": "Poista albumi",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Albumin poistaminen epäonnistui",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Albumista poistuminen epäonnistui",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Ongelmia kohteiden poistamisessa albumista",
|
||||||
|
"album_viewer_appbar_share_err_title": "Albumin nimen muuttaminen epäonnistui",
|
||||||
|
"album_viewer_appbar_share_leave": "Poistu albumista",
|
||||||
|
"album_viewer_appbar_share_remove": "Poista albumista",
|
||||||
|
"album_viewer_page_share_add_users": "Lisää käyttäjiä",
|
||||||
|
"backup_album_selection_page_albums_device": "Laitteen albumit ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Napauta sisällyttääksesi, kaksoisnapauta jättääksesi pois",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Kohteet voivat olla hajaantuneina useisiin albumeihin. Albumeita voidaan sisällyttää varmuuskopiointiin tai jättää siitä pois.",
|
||||||
|
"backup_album_selection_page_select_albums": "Valitse albumit",
|
||||||
|
"backup_album_selection_page_selection_info": "Valintatiedot",
|
||||||
|
"backup_album_selection_page_total_assets": "Uniikkeja kohteita yhteensä",
|
||||||
|
"backup_all": "Kaikki",
|
||||||
|
"backup_controller_page_albums": "Varmuuskopioi albumit",
|
||||||
|
"backup_controller_page_backup": "Varmuuskopioitu",
|
||||||
|
"backup_controller_page_backup_selected": "Valittu:",
|
||||||
|
"backup_controller_page_backup_sub": "Varmuuskopioidut kuvat ja videot",
|
||||||
|
"backup_controller_page_cancel": "Peruuta",
|
||||||
|
"backup_controller_page_created": "Luotu: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Kytke varmuuskopiointi päälle ladataksesi uudet kohteet palvelimelle automaattisesti.",
|
||||||
|
"backup_controller_page_excluded": "Jätetty pois:",
|
||||||
|
"backup_controller_page_failed": "Epäonnistui ({})",
|
||||||
|
"backup_controller_page_filename": "Tiedoston nimi: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Varmuuskopioinnin tiedot",
|
||||||
|
"backup_controller_page_none_selected": "Ei mitään",
|
||||||
|
"backup_controller_page_remainder": "Jäljellä",
|
||||||
|
"backup_controller_page_remainder_sub": "Varmuuskopiointia odottavat kuvat ja videot",
|
||||||
|
"backup_controller_page_select": "Valitse",
|
||||||
|
"backup_controller_page_server_storage": "Palvelimen tallennustila",
|
||||||
|
"backup_controller_page_start_backup": "Aloita varmuuskopiointi",
|
||||||
|
"backup_controller_page_status_off": "Varmuuskopiointi on pois päältä",
|
||||||
|
"backup_controller_page_status_on": "Varmuuskopiointi on päällä",
|
||||||
|
"backup_controller_page_storage_format": "{} / {} käytetty",
|
||||||
|
"backup_controller_page_to_backup": "Varmuuskopioitavat albumit",
|
||||||
|
"backup_controller_page_total": "Yhteensä",
|
||||||
|
"backup_controller_page_total_sub": "Kaikki uniikit kuvat ja videot valituista albumeista",
|
||||||
|
"backup_controller_page_turn_off": "Varmuuskopiointi pois päältä",
|
||||||
|
"backup_controller_page_turn_on": "Varmuuskopiointi päälle",
|
||||||
|
"backup_controller_page_uploading_file_info": "Tiedostojen lähetystiedot",
|
||||||
|
"backup_err_only_album": "Vähintään yhden albumin tulee olla valittuna",
|
||||||
|
"backup_info_card_assets": "kohdetta",
|
||||||
|
"control_bottom_app_bar_delete": "Poista",
|
||||||
|
"create_shared_album_page_share": "Jaa",
|
||||||
|
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
|
||||||
|
"create_shared_album_page_share_select_photos": "Valitse kuvat",
|
||||||
|
"daily_title_text_date": "",
|
||||||
|
"daily_title_text_date_year": "",
|
||||||
|
"date_format": "",
|
||||||
|
"delete_dialog_alert": "Nämä kohteet poistetaan pysyvästi Immich:stä ja laitteeltasi",
|
||||||
|
"delete_dialog_cancel": "Peruuta",
|
||||||
|
"delete_dialog_ok": "Poista",
|
||||||
|
"delete_dialog_title": "Poista pysyvästi",
|
||||||
|
"exif_bottom_sheet_description": "Lisää kuvaus…",
|
||||||
|
"exif_bottom_sheet_details": "TIEDOT",
|
||||||
|
"exif_bottom_sheet_location": "SIJAINTI",
|
||||||
|
"login_form_button_text": "Kirjaudu",
|
||||||
|
"login_form_email_hint": "sahkopostisi@esimerkki.fi",
|
||||||
|
"login_form_endpoint_hint": "http://palvelimesi-osoite:portti/api",
|
||||||
|
"login_form_endpoint_url": "Palvelimen URL",
|
||||||
|
"login_form_err_http": "Lisää http:// tai https://",
|
||||||
|
"login_form_err_invalid_email": "Virheellinen sähköpostiosoite",
|
||||||
|
"login_form_err_leading_whitespace": "Alussa välilyönti",
|
||||||
|
"login_form_err_trailing_whitespace": "Lopussa välilyönti",
|
||||||
|
"login_form_failed_login": "Virhe kirjautumisessa. Tarkista palvelimen URL, sähköpostiosoite ja salasana.",
|
||||||
|
"login_form_label_email": "Sähköposti",
|
||||||
|
"login_form_label_password": "Salasana",
|
||||||
|
"login_form_password_hint": "salasana",
|
||||||
|
"login_form_save_login": "Pysy kirjautuneena",
|
||||||
|
"monthly_title_text_date_format": "",
|
||||||
|
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
|
||||||
|
"profile_drawer_sign_out": "Kirjaudu ulos",
|
||||||
|
"search_bar_hint": "Etsi kuvia",
|
||||||
|
"search_page_no_objects": "Objektitietoja ei ole saatavilla",
|
||||||
|
"search_page_no_places": "Paikkatietoja ei ole saatavilla",
|
||||||
|
"search_page_places": "Paikat",
|
||||||
|
"search_page_things": "Asiat",
|
||||||
|
"search_result_page_new_search_hint": "Uusi haku",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Ehdotukset",
|
||||||
|
"select_user_for_sharing_page_err_album": "Albumin luonti epäonnistui",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Ehdotukset",
|
||||||
|
"share_add": "Lisää",
|
||||||
|
"share_add_photos": "Lisää kuvia",
|
||||||
|
"share_add_title": "Lisää nimi",
|
||||||
|
"share_create_album": "Luo albumi",
|
||||||
|
"share_invite": "Kutsu albumiin",
|
||||||
|
"sharing_page_album": "Jaetut albumit",
|
||||||
|
"sharing_page_description": "Luo jaettuja albumeja jakaaksesi kuvia ja videoita läheisillesi.",
|
||||||
|
"sharing_page_empty_list": "TYHJÄ LISTA",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Luo jaettu albumi",
|
||||||
|
"sharing_silver_appbar_share_partner": "Jaa kumppanille",
|
||||||
|
"tab_controller_nav_photos": "Kuvat",
|
||||||
|
"tab_controller_nav_search": "Haku",
|
||||||
|
"tab_controller_nav_sharing": "Jakaminen",
|
||||||
|
"version_announcement_overlay_ack": "Tiedostan",
|
||||||
|
"version_announcement_overlay_release_notes": "julkaisutiedoissa",
|
||||||
|
"version_announcement_overlay_text_1": "Hei, kaveri! Uusi palvelinversio on saatavilla sovelluksesta",
|
||||||
|
"version_announcement_overlay_text_2": "Ota hetki aikaa vieraillaksesi",
|
||||||
|
"version_announcement_overlay_text_3": "ja varmista, että käyttämäsi docker-compose ja .env-asetukset ovat ajantasalla välttyäksesi asetusongelmilta. Varsinkin jos käytät WatchToweria tai jotain muuta mekanismia päivittääksesi palvelinsovellusta automaattisesti.",
|
||||||
|
"version_announcement_overlay_title": "Uusi palvelinversio saatavilla \uD83C\uDF89"
|
||||||
|
}
|
||||||
106
mobile/assets/i18n/fr-FR.json
Normal file
106
mobile/assets/i18n/fr-FR.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "EXCLU",
|
||||||
|
"album_info_card_backup_album_included": "INCLUS",
|
||||||
|
"album_viewer_appbar_share_delete": "Supprimer l'album",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Il y a des problèmes pour retirer les éléments de l'album",
|
||||||
|
"album_viewer_appbar_share_err_title": "Échec de la modification du titre de l'album",
|
||||||
|
"album_viewer_appbar_share_leave": "Quitter l'album",
|
||||||
|
"album_viewer_appbar_share_remove": "Retirer de l'album",
|
||||||
|
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
|
||||||
|
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
|
||||||
|
"backup_album_selection_page_select_albums": "Sélectionner les albums",
|
||||||
|
"backup_album_selection_page_selection_info": "Informations sur la sélection",
|
||||||
|
"backup_album_selection_page_total_assets": "Total des éléments uniques",
|
||||||
|
"backup_all": "Tout",
|
||||||
|
"backup_controller_page_albums": "Sauvegarder les albums",
|
||||||
|
"backup_controller_page_backup": "Sauvegardé",
|
||||||
|
"backup_controller_page_backup_selected": "Sélectionné : ",
|
||||||
|
"backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
|
||||||
|
"backup_controller_page_cancel": "Annuler",
|
||||||
|
"backup_controller_page_created": "Créé le : {}",
|
||||||
|
"backup_controller_page_desc_backup": "Activez la sauvegarde pour envoyer automatiquement les nouveaux éléments sur le serveur.",
|
||||||
|
"backup_controller_page_excluded": "Exclus : ",
|
||||||
|
"backup_controller_page_failed": "Échec de l'opération ({})",
|
||||||
|
"backup_controller_page_filename": "Nom du fichier : {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID : {}",
|
||||||
|
"backup_controller_page_info": "Informations de sauvegarde",
|
||||||
|
"backup_controller_page_none_selected": "Aucune sélection",
|
||||||
|
"backup_controller_page_remainder": "Restant",
|
||||||
|
"backup_controller_page_remainder_sub": "Photos et albums restants à sauvegarder à partir de la sélection",
|
||||||
|
"backup_controller_page_select": "Sélectionner",
|
||||||
|
"backup_controller_page_server_storage": "Stockage du serveur",
|
||||||
|
"backup_controller_page_start_backup": "Démarrer la sauvegarde",
|
||||||
|
"backup_controller_page_status_off": "La sauvegarde est désactivée",
|
||||||
|
"backup_controller_page_status_on": "La sauvegarde est activée",
|
||||||
|
"backup_controller_page_storage_format": "{} de {} utilisé",
|
||||||
|
"backup_controller_page_to_backup": "Albums à sauvegarder",
|
||||||
|
"backup_controller_page_total": "Total",
|
||||||
|
"backup_controller_page_total_sub": "Toutes les photos et vidéos uniques des albums sélectionnés",
|
||||||
|
"backup_controller_page_turn_off": "Désactiver la sauvegarde",
|
||||||
|
"backup_controller_page_turn_on": "Activer la sauvegarde",
|
||||||
|
"backup_controller_page_uploading_file_info": "Envoi d'informations sur le fichier",
|
||||||
|
"backup_err_only_album": "Impossible de retirer le seul album",
|
||||||
|
"backup_info_card_assets": "éléments",
|
||||||
|
"control_bottom_app_bar_delete": "Supprimer",
|
||||||
|
"create_shared_album_page_share": "Partager",
|
||||||
|
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
|
||||||
|
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
|
||||||
|
"daily_title_text_date": "E, dd MMM",
|
||||||
|
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||||
|
"date_format": "E, LLL d, y • h:mm a",
|
||||||
|
"delete_dialog_alert": "Ces éléments seront définitivement supprimés de Immich et de votre appareil.",
|
||||||
|
"delete_dialog_cancel": "Annuler",
|
||||||
|
"delete_dialog_ok": "Supprimer",
|
||||||
|
"delete_dialog_title": "Supprimer définitivement",
|
||||||
|
"exif_bottom_sheet_description": "Ajouter une description...",
|
||||||
|
"exif_bottom_sheet_details": "DÉTAILS",
|
||||||
|
"exif_bottom_sheet_location": "LOCALISATION",
|
||||||
|
"login_form_button_text": "Connexion",
|
||||||
|
"login_form_email_hint": "votreemail@email.com",
|
||||||
|
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
|
||||||
|
"login_form_endpoint_url": "URL du point d'accès au serveur",
|
||||||
|
"login_form_err_http": "Veuillez préciser http:// ou https://",
|
||||||
|
"login_form_err_invalid_email": "Email invalide",
|
||||||
|
"login_form_err_leading_whitespace": "Espace en début de ligne",
|
||||||
|
"login_form_err_trailing_whitespace": "Espace de fin de ligne",
|
||||||
|
"login_form_failed_login": "Erreur de connexion, vérifiez l'url du serveur, l'email et le mot de passe",
|
||||||
|
"login_form_label_email": "Email",
|
||||||
|
"login_form_label_password": "Mot de passe",
|
||||||
|
"login_form_password_hint": "mot de passe",
|
||||||
|
"login_form_save_login": "Rester connecté",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
|
||||||
|
"profile_drawer_sign_out": "Se déconnecter",
|
||||||
|
"search_bar_hint": "Rechercher vos photos",
|
||||||
|
"search_page_no_objects": "Aucune information disponible sur les objets",
|
||||||
|
"search_page_no_places": "Aucune information disponible sur la localisation",
|
||||||
|
"search_page_places": "Lieux",
|
||||||
|
"search_page_things": "Objets",
|
||||||
|
"search_result_page_new_search_hint": "Nouvelle recherche",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
||||||
|
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||||
|
"share_add": "Ajouter",
|
||||||
|
"share_add_photos": "Ajouter des photos",
|
||||||
|
"share_add_title": "Ajouter un titre",
|
||||||
|
"share_create_album": "Créer un album",
|
||||||
|
"share_invite": "Inviter à l'album",
|
||||||
|
"sharing_page_album": "Albums partagés",
|
||||||
|
"sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
|
||||||
|
"sharing_page_empty_list": "LISTE VIDE",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Créer un album partagé",
|
||||||
|
"sharing_silver_appbar_share_partner": "Partager avec un partenaire",
|
||||||
|
"tab_controller_nav_photos": "Photos",
|
||||||
|
"tab_controller_nav_search": "Recherche",
|
||||||
|
"tab_controller_nav_sharing": "Partage",
|
||||||
|
"version_announcement_overlay_ack": "Confirmer",
|
||||||
|
"version_announcement_overlay_release_notes": "notes de mise à jour",
|
||||||
|
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
|
||||||
|
"version_announcement_overlay_text_2": "veuillez prendre le temps de visiter le ",
|
||||||
|
"version_announcement_overlay_text_3": " et assurez-vous que votre configuration docker-compose et .env est à jour pour éviter toute erreur de configuration, en particulier si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour automatique de votre application serveur.",
|
||||||
|
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89"
|
||||||
|
}
|
||||||
106
mobile/assets/i18n/it-IT.json
Normal file
106
mobile/assets/i18n/it-IT.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "ESCLUSI",
|
||||||
|
"album_info_card_backup_album_included": "INCLUSI",
|
||||||
|
"album_viewer_appbar_share_delete": "Elimina album ",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Fallito nel cancellare l'album ",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Fallito nel lasciare l'album ",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album ",
|
||||||
|
"album_viewer_appbar_share_err_title": "Fallito nel cambiare titolo dell'album ",
|
||||||
|
"album_viewer_appbar_share_leave": "Lascia l'album",
|
||||||
|
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
|
||||||
|
"album_viewer_page_share_add_users": "Aggiungi utenti",
|
||||||
|
"backup_album_selection_page_albums_device": "Albums nel device ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Stesse immagini e video possono trovarsi tra più album, così gli album possono essere inclusi o esclusi dal backup.",
|
||||||
|
"backup_album_selection_page_select_albums": "Seleziona gli album",
|
||||||
|
"backup_album_selection_page_selection_info": "Informazioni sulla selezione ",
|
||||||
|
"backup_album_selection_page_total_assets": "Numero totale di oggetti unici",
|
||||||
|
"backup_all": "Tutti",
|
||||||
|
"backup_controller_page_albums": "Backup album",
|
||||||
|
"backup_controller_page_backup": "Backup",
|
||||||
|
"backup_controller_page_backup_selected": "Selezionati:",
|
||||||
|
"backup_controller_page_backup_sub": "Photo e video salvati",
|
||||||
|
"backup_controller_page_cancel": "Cancella ",
|
||||||
|
"backup_controller_page_created": "Creato il: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Attiva il backup automatico per eseguire upload sul server",
|
||||||
|
"backup_controller_page_excluded": "Esclusi:",
|
||||||
|
"backup_controller_page_failed": "Falliti: ({})",
|
||||||
|
"backup_controller_page_filename": "Nome del file: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Informazioni sul backup",
|
||||||
|
"backup_controller_page_none_selected": "Nessuna selezione",
|
||||||
|
"backup_controller_page_remainder": "Promemoria ",
|
||||||
|
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da salvare",
|
||||||
|
"backup_controller_page_select": "Seleziona ",
|
||||||
|
"backup_controller_page_server_storage": "Spazio nel server",
|
||||||
|
"backup_controller_page_start_backup": "Inizia backup ",
|
||||||
|
"backup_controller_page_status_off": "Backup è disattivato ",
|
||||||
|
"backup_controller_page_status_on": "Backup è attivato",
|
||||||
|
"backup_controller_page_storage_format": "{} di {} usati",
|
||||||
|
"backup_controller_page_to_backup": "Album da salvare",
|
||||||
|
"backup_controller_page_total": "Totale",
|
||||||
|
"backup_controller_page_total_sub": "Tutte le foto e i video unici salvati dagli album selezionati ",
|
||||||
|
"backup_controller_page_turn_off": "Disattiva backup",
|
||||||
|
"backup_controller_page_turn_on": "Attiva backup ",
|
||||||
|
"backup_controller_page_uploading_file_info": "Info sul file caricato",
|
||||||
|
"backup_err_only_album": "Non è possibile rimuovere l'unico album",
|
||||||
|
"backup_info_card_assets": "Oggetti ",
|
||||||
|
"control_bottom_app_bar_delete": "Elimina",
|
||||||
|
"create_shared_album_page_share": "Condividi",
|
||||||
|
"create_shared_album_page_share_add_assets": "AGGIUNGI OGGETTI",
|
||||||
|
"create_shared_album_page_share_select_photos": "Seleziona foto",
|
||||||
|
"daily_title_text_date": "E, dd MMM",
|
||||||
|
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||||
|
"date_format": "E, d LLL, y • hh:mm",
|
||||||
|
"delete_dialog_alert": "Questi oggetti saranno cancellati permanentemente da Immich e dal tuo device",
|
||||||
|
"delete_dialog_cancel": "Annulla",
|
||||||
|
"delete_dialog_ok": "Elimina",
|
||||||
|
"delete_dialog_title": "Cancella in modo permanente ",
|
||||||
|
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
|
||||||
|
"exif_bottom_sheet_details": "DETTAGLI",
|
||||||
|
"exif_bottom_sheet_location": "POSIZIONE",
|
||||||
|
"login_form_button_text": "Accedi",
|
||||||
|
"login_form_email_hint": "tuaemail@email.com",
|
||||||
|
"login_form_endpoint_hint": "http://tuo-ip-del-server:port/api",
|
||||||
|
"login_form_endpoint_url": "URL del Server Endpoint",
|
||||||
|
"login_form_err_http": "Per favore specificare http:// o https://",
|
||||||
|
"login_form_err_invalid_email": "Email non valida",
|
||||||
|
"login_form_err_leading_whitespace": "Spazio bianco all'inizio ",
|
||||||
|
"login_form_err_trailing_whitespace": "Spazio bianco alla fine",
|
||||||
|
"login_form_failed_login": "Errore nel login, controlla URL del server e le credenziali (email e password)",
|
||||||
|
"login_form_label_email": "Email",
|
||||||
|
"login_form_label_password": "Password",
|
||||||
|
"login_form_password_hint": "password ",
|
||||||
|
"login_form_save_login": "Rimani connesso ",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
|
||||||
|
"profile_drawer_sign_out": "Esci",
|
||||||
|
"search_bar_hint": "Cerca le tue foto",
|
||||||
|
"search_page_no_objects": "Nessuna Informazione relativa all'Oggetto Disponibile",
|
||||||
|
"search_page_no_places": "Nessun informazione sulla posizione ",
|
||||||
|
"search_page_places": "Luoghi",
|
||||||
|
"search_page_things": "Oggetti",
|
||||||
|
"search_result_page_new_search_hint": "Nuova ricerca ",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Suggerimenti ",
|
||||||
|
"select_user_for_sharing_page_err_album": "Fallito nel creare l'album ",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Suggerimenti",
|
||||||
|
"share_add": "Aggiungi",
|
||||||
|
"share_add_photos": "Aggiungi foto",
|
||||||
|
"share_add_title": "Aggiungi un titolo ",
|
||||||
|
"share_create_album": "Crea album",
|
||||||
|
"share_invite": "Invitare all'album ",
|
||||||
|
"sharing_page_album": "Album condivisi",
|
||||||
|
"sharing_page_description": "Crea un album condiviso per condividere foto e video con gente nel tuo network",
|
||||||
|
"sharing_page_empty_list": "LISTA VUOTA",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Crea album condiviso",
|
||||||
|
"sharing_silver_appbar_share_partner": "Condividi con il partner",
|
||||||
|
"tab_controller_nav_photos": "Foto",
|
||||||
|
"tab_controller_nav_search": "Cerca",
|
||||||
|
"tab_controller_nav_sharing": "Condividi",
|
||||||
|
"version_announcement_overlay_ack": "Riconosci ",
|
||||||
|
"version_announcement_overlay_release_notes": "le note di rilascio ",
|
||||||
|
"version_announcement_overlay_text_1": "Ciao amico, c'è una nuova versione di",
|
||||||
|
"version_announcement_overlay_text_2": "prova a controllare ",
|
||||||
|
"version_announcement_overlay_text_3": "e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore nella configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico delle immagini docker.",
|
||||||
|
"version_announcement_overlay_title": "Nuova versione di server disponibile! \uD83C\uDF89"
|
||||||
|
}
|
||||||
106
mobile/assets/i18n/ja-JP.json
Normal file
106
mobile/assets/i18n/ja-JP.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "除外",
|
||||||
|
"album_info_card_backup_album_included": "選択",
|
||||||
|
"album_viewer_appbar_share_delete": "アルバムを削除",
|
||||||
|
"album_viewer_appbar_share_err_delete": "削除に失敗...",
|
||||||
|
"album_viewer_appbar_share_err_leave": "退会に失敗...",
|
||||||
|
"album_viewer_appbar_share_err_remove": "アルバムから写真を除外する際にエラー発生",
|
||||||
|
"album_viewer_appbar_share_err_title": "タイトルの変更に失敗...",
|
||||||
|
"album_viewer_appbar_share_leave": "アルバムから退会",
|
||||||
|
"album_viewer_appbar_share_remove": "アルバムから除外",
|
||||||
|
"album_viewer_page_share_add_users": "ユーザーを追加",
|
||||||
|
"backup_album_selection_page_albums_device": "端末上のアルバム数は {} だよ",
|
||||||
|
"backup_album_selection_page_albums_tap": "タップで選択、ダブルタップで除外だよ",
|
||||||
|
"backup_album_selection_page_assets_scatter": "写真がいろんなアルバムに登録されてる事があるから、アルバムを含めたり除外したりしてどの写真を保存するか選択できるよ。",
|
||||||
|
"backup_album_selection_page_select_albums": "アルバムを選択",
|
||||||
|
"backup_album_selection_page_selection_info": "選択、又は除外されてるアルバム",
|
||||||
|
"backup_album_selection_page_total_assets": "選択されたアルバムの写真と動画の数",
|
||||||
|
"backup_all": "全て",
|
||||||
|
"backup_controller_page_albums": "アルバム",
|
||||||
|
"backup_controller_page_backup": "バックアップ",
|
||||||
|
"backup_controller_page_backup_selected": "選択されてる:",
|
||||||
|
"backup_controller_page_backup_sub": "バックアップされた写真と動画の数だよ",
|
||||||
|
"backup_controller_page_cancel": "キャンセルするよ",
|
||||||
|
"backup_controller_page_created": "{} に作成されたよ",
|
||||||
|
"backup_controller_page_desc_backup": "ONにすれば自動的に新しい写真などがバックアップされるようになるよ",
|
||||||
|
"backup_controller_page_excluded": "除外されてるアルバム:",
|
||||||
|
"backup_controller_page_failed": "失敗: ({})",
|
||||||
|
"backup_controller_page_filename": "ファイル名: {} [{}] ",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "バックアップ情報",
|
||||||
|
"backup_controller_page_none_selected": "何も選んでないよ",
|
||||||
|
"backup_controller_page_remainder": "リマインダー",
|
||||||
|
"backup_controller_page_remainder_sub": "残りの写真と動画の数だよ",
|
||||||
|
"backup_controller_page_select": "選択",
|
||||||
|
"backup_controller_page_server_storage": "サーバーの容量",
|
||||||
|
"backup_controller_page_start_backup": "バックアップを開始するよ",
|
||||||
|
"backup_controller_page_status_off": "バックアップがOFFになってるよ",
|
||||||
|
"backup_controller_page_status_on": "バックアップがONになってるよ",
|
||||||
|
"backup_controller_page_storage_format": "{}中、 {}を使用中だよ",
|
||||||
|
"backup_controller_page_to_backup": "バックアップされるアルバム",
|
||||||
|
"backup_controller_page_total": "トータル",
|
||||||
|
"backup_controller_page_total_sub": "選択されたアルバムの写真と動画の数だよ",
|
||||||
|
"backup_controller_page_turn_off": "バックアップOFF",
|
||||||
|
"backup_controller_page_turn_on": "バックアップON",
|
||||||
|
"backup_controller_page_uploading_file_info": "アップロードされてるファイルに関する情報",
|
||||||
|
"backup_err_only_album": "唯一のアルバムを除外する事はできないよ",
|
||||||
|
"backup_info_card_assets": "写真と動画",
|
||||||
|
"control_bottom_app_bar_delete": "削除",
|
||||||
|
"create_shared_album_page_share": "共有",
|
||||||
|
"create_shared_album_page_share_add_assets": "写真や動画を追加",
|
||||||
|
"create_shared_album_page_share_select_photos": "写真を選択",
|
||||||
|
"daily_title_text_date": "E, MM月 dd日",
|
||||||
|
"daily_title_text_date_year": "E, yyyy年 MM月 dd日",
|
||||||
|
"date_format": "E, MM月 dd日 • hh時mm分",
|
||||||
|
"delete_dialog_alert": "サーバーからも端末からも永久的に削除されるけど良いの?",
|
||||||
|
"delete_dialog_cancel": "キャンセル",
|
||||||
|
"delete_dialog_ok": "削除",
|
||||||
|
"delete_dialog_title": "永久的に削除",
|
||||||
|
"exif_bottom_sheet_description": "概要を追加",
|
||||||
|
"exif_bottom_sheet_details": "詳細な情報",
|
||||||
|
"exif_bottom_sheet_location": "撮影地",
|
||||||
|
"login_form_button_text": "ログイン",
|
||||||
|
"login_form_email_hint": "example@email.com",
|
||||||
|
"login_form_endpoint_hint": "https://example.com:port/api",
|
||||||
|
"login_form_endpoint_url": "サーバーエンドポイントURL",
|
||||||
|
"login_form_err_http": "http://かhttps://かを指定してね",
|
||||||
|
"login_form_err_invalid_email": "メールアドレスが有効じゃないよ",
|
||||||
|
"login_form_err_leading_whitespace": "最初に半角スペースが含まれてるよ",
|
||||||
|
"login_form_err_trailing_whitespace": "最後に半角スペースが含まれてるよ",
|
||||||
|
"login_form_failed_login": "ログインエラー。サーバーのURL、メールアドレスとパスワードを再確認してね",
|
||||||
|
"login_form_label_email": "メールアドレス",
|
||||||
|
"login_form_label_password": "パスワード",
|
||||||
|
"login_form_password_hint": "パスワード",
|
||||||
|
"login_form_save_login": "ログインしたままにする",
|
||||||
|
"monthly_title_text_date_format": "yyyy年 MM月",
|
||||||
|
"profile_drawer_client_server_up_to_date": "サーバーとクライアント、両方最新バージョンだよ",
|
||||||
|
"profile_drawer_sign_out": "サインアウト",
|
||||||
|
"search_bar_hint": "写真を検索",
|
||||||
|
"search_page_no_objects": "被写体に関するデータがないよ",
|
||||||
|
"search_page_no_places": "場所に関するデータがないよ",
|
||||||
|
"search_page_places": "撮影地",
|
||||||
|
"search_page_things": "カテゴリ",
|
||||||
|
"search_result_page_new_search_hint": "検索",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "ユーザーリスト",
|
||||||
|
"select_user_for_sharing_page_err_album": "アルバム作成に失敗...",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "ユーザーの一覧だよ",
|
||||||
|
"share_add": "追加",
|
||||||
|
"share_add_photos": "写真を追加",
|
||||||
|
"share_add_title": "タイトルを追加",
|
||||||
|
"share_create_album": "アルバムを作成",
|
||||||
|
"share_invite": "アルバムに参加",
|
||||||
|
"sharing_page_album": "共有アルバム",
|
||||||
|
"sharing_page_description": "共有アルバムを作成して同じネットワークにいる仲間に写真を共有してみよう!",
|
||||||
|
"sharing_page_empty_list": "誰も居ないね ( T_T)\(^-^ ) ドンマイ",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "共有アルバムを作成",
|
||||||
|
"sharing_silver_appbar_share_partner": "パートナーと共有",
|
||||||
|
"tab_controller_nav_photos": "写真",
|
||||||
|
"tab_controller_nav_search": "検索",
|
||||||
|
"tab_controller_nav_sharing": "共有",
|
||||||
|
"version_announcement_overlay_ack": "了解",
|
||||||
|
"version_announcement_overlay_release_notes": "更新情報",
|
||||||
|
"version_announcement_overlay_text_1": "こんにちは、又はこんばんは!新しい",
|
||||||
|
"version_announcement_overlay_text_2": "のバージョンが公開中だよ。",
|
||||||
|
"version_announcement_overlay_text_3": "を確認してみてね。あと、docker-composeや.envファイルが最新の状態に更新されてか、特にWatchTowerなどのツールを使ってDockerイメージを自動アップデートしてる人は確認してね",
|
||||||
|
"version_announcement_overlay_title": "新しいバージョン、公開中\uD83C\uDF89"
|
||||||
|
}
|
||||||
106
mobile/assets/i18n/pl-PL.json
Normal file
106
mobile/assets/i18n/pl-PL.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "WYKLUCZONE",
|
||||||
|
"album_info_card_backup_album_included": "WŁĄCZONE",
|
||||||
|
"album_viewer_appbar_share_delete": "Usuń album",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Nie udało się usunąć albumu",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Nie udało się wyjść z albumu",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Wystąpiły problemy z usunięciem plików z albumu",
|
||||||
|
"album_viewer_appbar_share_err_title": "Nie udało się zmienić tytułu albumu",
|
||||||
|
"album_viewer_appbar_share_leave": "Opuść album",
|
||||||
|
"album_viewer_appbar_share_remove": "Usuń z albumu",
|
||||||
|
"album_viewer_page_share_add_users": "Dodaj użytkowników",
|
||||||
|
"backup_album_selection_page_albums_device": "Albumy na urządzeniu ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.",
|
||||||
|
"backup_album_selection_page_select_albums": "Zaznacz albumy",
|
||||||
|
"backup_album_selection_page_selection_info": "Info o wyborze",
|
||||||
|
"backup_album_selection_page_total_assets": "Łącznie unikalnych plików",
|
||||||
|
"backup_all": "Wszystkie",
|
||||||
|
"backup_controller_page_albums": "Backup Albumów",
|
||||||
|
"backup_controller_page_backup": "Backup",
|
||||||
|
"backup_controller_page_backup_selected": "Zaznaczone: ",
|
||||||
|
"backup_controller_page_backup_sub": "Tworzenie kopii zapasowych zdjęć i filmów",
|
||||||
|
"backup_controller_page_cancel": "Anuluj",
|
||||||
|
"backup_controller_page_created": "Utworzony na: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Włącz backup, aby automatycznie przesyłać nowe zasoby na serwer.",
|
||||||
|
"backup_controller_page_excluded": "Wykluczone: ",
|
||||||
|
"backup_controller_page_failed": "Nieudane ({})",
|
||||||
|
"backup_controller_page_filename": "Nazwa pliku: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Informacje o kopii zapasowej",
|
||||||
|
"backup_controller_page_none_selected": "Brak wybranych",
|
||||||
|
"backup_controller_page_remainder": "Reszta",
|
||||||
|
"backup_controller_page_remainder_sub": "Pozostałe zdjęcia i albumy do wykonania kopii zapasowej z wyboru",
|
||||||
|
"backup_controller_page_select": "Zaznacz",
|
||||||
|
"backup_controller_page_server_storage": "Pamięć Serwera",
|
||||||
|
"backup_controller_page_start_backup": "Rozpocznij Backup",
|
||||||
|
"backup_controller_page_status_off": "Backup jest wyłączony",
|
||||||
|
"backup_controller_page_status_on": "Backup jest włączony",
|
||||||
|
"backup_controller_page_storage_format": "{} z {} wykorzystanych",
|
||||||
|
"backup_controller_page_to_backup": "Albumy do backupu",
|
||||||
|
"backup_controller_page_total": "Łącznie",
|
||||||
|
"backup_controller_page_total_sub": "Wszystkie unikalne zdjęcia i filmy z wybranych albumów",
|
||||||
|
"backup_controller_page_turn_off": "Wyłącz Backup",
|
||||||
|
"backup_controller_page_turn_on": "Włącz Backup",
|
||||||
|
"backup_controller_page_uploading_file_info": "Przesyłanie informacji o pliku",
|
||||||
|
"backup_err_only_album": "Nie można usunąć tylko i wyłącznie albumu",
|
||||||
|
"backup_info_card_assets": "pliki",
|
||||||
|
"control_bottom_app_bar_delete": "Usuń",
|
||||||
|
"create_shared_album_page_share": "Udostępnij",
|
||||||
|
"create_shared_album_page_share_add_assets": "DODAJ PLIKI",
|
||||||
|
"create_shared_album_page_share_select_photos": "Zaznacz Zdjęcia",
|
||||||
|
"daily_title_text_date": "E, MMM dd",
|
||||||
|
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||||
|
"date_format": "E, LLL d, y • h:mm a",
|
||||||
|
"delete_dialog_alert": "Te elementy zostaną trwale usunięte z Immich i z Twojego urządzenia",
|
||||||
|
"delete_dialog_cancel": "Anuluj",
|
||||||
|
"delete_dialog_ok": "Usuń",
|
||||||
|
"delete_dialog_title": "Usuń trwale",
|
||||||
|
"exif_bottom_sheet_description": "Dodaj opis...",
|
||||||
|
"exif_bottom_sheet_details": "SZCZEGÓŁY",
|
||||||
|
"exif_bottom_sheet_location": "LOKALIZACJA",
|
||||||
|
"login_form_button_text": "Login",
|
||||||
|
"login_form_email_hint": "twojmail@email.com",
|
||||||
|
"login_form_endpoint_hint": "http://ip-twojego-serwera:port/api",
|
||||||
|
"login_form_endpoint_url": "URL Serwera",
|
||||||
|
"login_form_err_http": "Proszę określić http:// lub https://",
|
||||||
|
"login_form_err_invalid_email": "Niepoprawny emaill",
|
||||||
|
"login_form_err_leading_whitespace": "Białe znaki",
|
||||||
|
"login_form_err_trailing_whitespace": "Białe znaki po przecinku",
|
||||||
|
"login_form_failed_login": "Błąd logowania, sprawdź adres url serwera, email i hasło.",
|
||||||
|
"login_form_label_email": "Email",
|
||||||
|
"login_form_label_password": "Hasło",
|
||||||
|
"login_form_password_hint": "hasło",
|
||||||
|
"login_form_save_login": "Pozostań zalogowany",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne",
|
||||||
|
"profile_drawer_sign_out": "Wyloguj się",
|
||||||
|
"search_bar_hint": "Szukaj swoich zdjęć",
|
||||||
|
"search_page_no_objects": "Brak informacji o obiektach",
|
||||||
|
"search_page_no_places": "Brak informacji o miejscu",
|
||||||
|
"search_page_places": "Miejsca",
|
||||||
|
"search_page_things": "Rzeczy",
|
||||||
|
"search_result_page_new_search_hint": "Nowe wyszukiwanie",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Propozycje",
|
||||||
|
"select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Propozycje",
|
||||||
|
"share_add": "Dodaj",
|
||||||
|
"share_add_photos": "Dodaj zdjęcia",
|
||||||
|
"share_add_title": "Dodaj tytuł",
|
||||||
|
"share_create_album": "Utwórz album",
|
||||||
|
"share_invite": "Zaproś do albumu",
|
||||||
|
"sharing_page_album": "Udostępnione albumy",
|
||||||
|
"sharing_page_description": "Twórz wspóldzielone albumy, aby udostępniać zdjęcia i filmy osobom w sieci.",
|
||||||
|
"sharing_page_empty_list": "PUSTA LISTA",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Utwórz współdzielony album",
|
||||||
|
"sharing_silver_appbar_share_partner": "Udostępnij partnerce/partnerowi",
|
||||||
|
"tab_controller_nav_photos": "Zdjęcia",
|
||||||
|
"tab_controller_nav_search": "Szukaj",
|
||||||
|
"tab_controller_nav_sharing": "Udostępnianie",
|
||||||
|
"version_announcement_overlay_ack": "Potwierdzenie",
|
||||||
|
"version_announcement_overlay_release_notes": "informacje o wydaniu",
|
||||||
|
"version_announcement_overlay_text_1": "Cześć przyjacielu, jest nowe wydanie",
|
||||||
|
"version_announcement_overlay_text_2": "prosimy o poświęcenie czasu na odwiedzenie ",
|
||||||
|
"version_announcement_overlay_text_3": " i upewnij się, że twoja konfiguracja docker-compose i .env jest aktualna, aby zapobiec błędnym konfiguracjom, zwłaszcza jeśli używasz WatchTower lub dowolnego mechanizmu, który obsługuje automatyczną aktualizację aplikacji serwera.",
|
||||||
|
"version_announcement_overlay_title": "Nowa wersja serwera dostępna \uD83C\uDF89"
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
|
- shared_preferences_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- sqflite (0.0.2):
|
- sqflite (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FMDB (>= 2.7.5)
|
||||||
@@ -38,6 +40,7 @@ DEPENDENCIES:
|
|||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
|
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
||||||
@@ -64,6 +67,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
|
shared_preferences_ios:
|
||||||
|
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
@@ -83,6 +88,7 @@ SPEC CHECKSUMS:
|
|||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
|
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||||
|
|||||||
@@ -360,7 +360,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 14;
|
CURRENT_PROJECT_VERSION = 38;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 14;
|
CURRENT_PROJECT_VERSION = 38;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -522,7 +522,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 14;
|
CURRENT_PROJECT_VERSION = 38;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.10.0</string>
|
<string>1.20.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>14</string>
|
<string>38</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true />
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
@@ -82,5 +82,18 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>https</string>
|
<string>https</string>
|
||||||
</array>
|
</array>
|
||||||
|
|
||||||
|
<key>CFBundleLocalizations</key>
|
||||||
|
<array>
|
||||||
|
<string>en</string>
|
||||||
|
<string>de</string>
|
||||||
|
<string>da</string>
|
||||||
|
<string>es</string>
|
||||||
|
<string>fr</string>
|
||||||
|
<string>it</string>
|
||||||
|
<string>fi</string>
|
||||||
|
<string>ja</string>
|
||||||
|
<string>pl</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.16.0"
|
version_number: "1.20.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -5,12 +5,34 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000946">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000213">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="16.3225">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.088407">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="22.635867">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.376681">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="4: build_app" time="91.762747">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="49.149884">
|
||||||
|
|
||||||
|
<failure message="/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `chdir' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:30:in `block (2 levels) in parsing_binding' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane.rb:33:in `call' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `chdir' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `execute' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/bin/fastlane:23:in `<top (required)>' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `load' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `<main>' Error uploading ipa file: [Transporter Error Output]: ERROR ITMS-90186: Invalid Pre-Release Train. The train version '1.19.0' is closed for new build submissions
|
||||||
[Transporter Error Output]: ERROR ITMS-90062: This bundle is invalid. The value for key CFBundleShortVersionString [1.19.0] in the Info.plist file must contain a higher version than that of the previously approved version [1.19.0]. Please find more information about CFBundleShortVersionString at https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
|
[Transporter Error Output]: ERROR ITMS-90062: This bundle is invalid. The value for key CFBundleShortVersionString [1.19.0] in the Info.plist file must contain a higher version than that of the previously approved version [1.19.0]. Please find more information about CFBundleShortVersionString at https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
|
||||||
[Transporter Error Output]: Return status of iTunes Transporter was 1: ERROR ITMS-90186: Invalid Pre-Release Train. The train version '1.19.0' is closed for new build submissions
|
[Transporter Error Output]: Return status of iTunes Transporter was 1: ERROR ITMS-90186: Invalid Pre-Release Train. The train version '1.19.0' is closed for new build submissions
|
||||||
\nERROR ITMS-90062: This bundle is invalid. The value for key CFBundleShortVersionString [1.19.0] in the Info.plist file must contain a higher version than that of the previously approved version [1.19.0]. Please find more information about CFBundleShortVersionString at https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
|
\nERROR ITMS-90062: This bundle is invalid. The value for key CFBundleShortVersionString [1.19.0] in the Info.plist file must contain a higher version than that of the previously approved version [1.19.0]. Please find more information about CFBundleShortVersionString at https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
@@ -16,7 +17,6 @@ import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
|||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
||||||
|
|
||||||
import 'constants/hive_box.dart';
|
import 'constants/hive_box.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
@@ -36,7 +36,28 @@ void main() async {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(const ProviderScope(child: ImmichApp()));
|
await EasyLocalization.ensureInitialized();
|
||||||
|
|
||||||
|
var locales = const [
|
||||||
|
// Default locale
|
||||||
|
Locale('en', 'US'),
|
||||||
|
// Additional locales
|
||||||
|
Locale('da', 'DK'),
|
||||||
|
Locale('de', 'DE'),
|
||||||
|
Locale('es', 'ES'),
|
||||||
|
Locale('fr', 'FR'),
|
||||||
|
Locale('it', 'IT'),
|
||||||
|
];
|
||||||
|
|
||||||
|
runApp(
|
||||||
|
EasyLocalization(
|
||||||
|
supportedLocales: locales,
|
||||||
|
path: 'assets/i18n',
|
||||||
|
useFallbackTranslations: true,
|
||||||
|
fallbackLocale: locales.first,
|
||||||
|
child: const ProviderScope(child: ImmichApp()),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImmichApp extends ConsumerStatefulWidget {
|
class ImmichApp extends ConsumerStatefulWidget {
|
||||||
@@ -96,6 +117,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
initApp().then((_) => debugPrint("App Init Completed"));
|
initApp().then((_) => debugPrint("App Init Completed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,13 +127,15 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
final _immichRouter = AppRouter();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var router = ref.watch(appRouterProvider);
|
||||||
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
localizationsDelegates: context.localizationDelegates,
|
||||||
|
supportedLocales: context.supportedLocales,
|
||||||
|
locale: context.locale,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
home: Stack(
|
home: Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -124,7 +148,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
primarySwatch: Colors.indigo,
|
primarySwatch: Colors.indigo,
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
snackBarTheme: const SnackBarThemeData(
|
snackBarTheme: const SnackBarThemeData(
|
||||||
contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
|
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
|
||||||
|
),
|
||||||
scaffoldBackgroundColor: immichBackgroundColor,
|
scaffoldBackgroundColor: immichBackgroundColor,
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: immichBackgroundColor,
|
backgroundColor: immichBackgroundColor,
|
||||||
@@ -134,9 +159,10 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
routeInformationParser: _immichRouter.defaultRouteParser(),
|
routeInformationParser: router.defaultRouteParser(),
|
||||||
routerDelegate: _immichRouter.delegate(
|
routerDelegate: router.delegate(
|
||||||
navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
|
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const ImmichLoadingOverlay(),
|
const ImmichLoadingOverlay(),
|
||||||
const VersionAnnouncementOverlay(),
|
const VersionAnnouncementOverlay(),
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetSelectionPageResult {
|
class AssetSelectionPageResult {
|
||||||
final Set<ImmichAsset> selectedNewAsset;
|
final Set<AssetResponseDto> selectedNewAsset;
|
||||||
final Set<ImmichAsset> selectedAdditionalAsset;
|
final Set<AssetResponseDto> selectedAdditionalAsset;
|
||||||
final bool isAlbumExist;
|
final bool isAlbumExist;
|
||||||
|
|
||||||
AssetSelectionPageResult({
|
AssetSelectionPageResult({
|
||||||
@@ -16,8 +14,8 @@ class AssetSelectionPageResult {
|
|||||||
});
|
});
|
||||||
|
|
||||||
AssetSelectionPageResult copyWith({
|
AssetSelectionPageResult copyWith({
|
||||||
Set<ImmichAsset>? selectedNewAsset,
|
Set<AssetResponseDto>? selectedNewAsset,
|
||||||
Set<ImmichAsset>? selectedAdditionalAsset,
|
Set<AssetResponseDto>? selectedAdditionalAsset,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
return AssetSelectionPageResult(
|
return AssetSelectionPageResult(
|
||||||
@@ -28,35 +26,6 @@ class AssetSelectionPageResult {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll(
|
|
||||||
{'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()});
|
|
||||||
result.addAll({
|
|
||||||
'selectedAdditionalAsset':
|
|
||||||
selectedAdditionalAsset.map((x) => x.toMap()).toList()
|
|
||||||
});
|
|
||||||
result.addAll({'isAlbumExist': isAlbumExist});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) {
|
|
||||||
return AssetSelectionPageResult(
|
|
||||||
selectedNewAsset: Set<ImmichAsset>.from(
|
|
||||||
map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedAdditionalAsset: Set<ImmichAsset>.from(
|
|
||||||
map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
isAlbumExist: map['isAlbumExist'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory AssetSelectionPageResult.fromJson(String source) =>
|
|
||||||
AssetSelectionPageResult.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';
|
'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetSelectionState {
|
class AssetSelectionState {
|
||||||
final Set<String> selectedMonths;
|
final Set<String> selectedMonths;
|
||||||
final Set<ImmichAsset> selectedNewAssetsForAlbum;
|
final Set<AssetResponseDto> selectedNewAssetsForAlbum;
|
||||||
final Set<ImmichAsset> selectedAdditionalAssetsForAlbum;
|
final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum;
|
||||||
final Set<ImmichAsset> selectedAssetsInAlbumViewer;
|
final Set<AssetResponseDto> selectedAssetsInAlbumViewer;
|
||||||
final bool isMultiselectEnable;
|
final bool isMultiselectEnable;
|
||||||
|
|
||||||
/// Indicate the asset selection page is navigated from existing album
|
/// Indicate the asset selection page is navigated from existing album
|
||||||
@@ -24,9 +22,9 @@ class AssetSelectionState {
|
|||||||
|
|
||||||
AssetSelectionState copyWith({
|
AssetSelectionState copyWith({
|
||||||
Set<String>? selectedMonths,
|
Set<String>? selectedMonths,
|
||||||
Set<ImmichAsset>? selectedNewAssetsForAlbum,
|
Set<AssetResponseDto>? selectedNewAssetsForAlbum,
|
||||||
Set<ImmichAsset>? selectedAdditionalAssetsForAlbum,
|
Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum,
|
||||||
Set<ImmichAsset>? selectedAssetsInAlbumViewer,
|
Set<AssetResponseDto>? selectedAssetsInAlbumViewer,
|
||||||
bool? isMultiselectEnable,
|
bool? isMultiselectEnable,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
@@ -43,49 +41,6 @@ class AssetSelectionState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'selectedMonths': selectedMonths.toList()});
|
|
||||||
result.addAll({
|
|
||||||
'selectedNewAssetsForAlbum':
|
|
||||||
selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()
|
|
||||||
});
|
|
||||||
result.addAll({
|
|
||||||
'selectedAdditionalAssetsForAlbum':
|
|
||||||
selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList()
|
|
||||||
});
|
|
||||||
result.addAll({
|
|
||||||
'selectedAssetsInAlbumViewer':
|
|
||||||
selectedAssetsInAlbumViewer.map((x) => x.toMap()).toList()
|
|
||||||
});
|
|
||||||
result.addAll({'isMultiselectEnable': isMultiselectEnable});
|
|
||||||
result.addAll({'isAlbumExist': isAlbumExist});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AssetSelectionState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return AssetSelectionState(
|
|
||||||
selectedMonths: Set<String>.from(map['selectedMonths']),
|
|
||||||
selectedNewAssetsForAlbum: Set<ImmichAsset>.from(
|
|
||||||
map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedAdditionalAssetsForAlbum: Set<ImmichAsset>.from(
|
|
||||||
map['selectedAdditionalAssetsForAlbum']
|
|
||||||
?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedAssetsInAlbumViewer: Set<ImmichAsset>.from(
|
|
||||||
map['selectedAssetsInAlbumViewer']
|
|
||||||
?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
isMultiselectEnable: map['isMultiselectEnable'] ?? false,
|
|
||||||
isAlbumExist: map['isAlbumExist'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory AssetSelectionState.fromJson(String source) =>
|
|
||||||
AssetSelectionState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)';
|
return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)';
|
||||||
@@ -99,10 +54,14 @@ class AssetSelectionState {
|
|||||||
return other is AssetSelectionState &&
|
return other is AssetSelectionState &&
|
||||||
setEquals(other.selectedMonths, selectedMonths) &&
|
setEquals(other.selectedMonths, selectedMonths) &&
|
||||||
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
|
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
|
||||||
setEquals(other.selectedAdditionalAssetsForAlbum,
|
|
||||||
selectedAdditionalAssetsForAlbum) &&
|
|
||||||
setEquals(
|
setEquals(
|
||||||
other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) &&
|
other.selectedAdditionalAssetsForAlbum,
|
||||||
|
selectedAdditionalAssetsForAlbum,
|
||||||
|
) &&
|
||||||
|
setEquals(
|
||||||
|
other.selectedAssetsInAlbumViewer,
|
||||||
|
selectedAssetsInAlbumViewer,
|
||||||
|
) &&
|
||||||
other.isMultiselectEnable == isMultiselectEnable &&
|
other.isMultiselectEnable == isMultiselectEnable &&
|
||||||
other.isAlbumExist == isAlbumExist;
|
other.isAlbumExist == isAlbumExist;
|
||||||
}
|
}
|
||||||
40
mobile/lib/modules/album/providers/album.provider.dart
Normal file
40
mobile/lib/modules/album/providers/album.provider.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
|
AlbumNotifier(this._albumService) : super([]);
|
||||||
|
final AlbumService _albumService;
|
||||||
|
|
||||||
|
getAllAlbums() async {
|
||||||
|
List<AlbumResponseDto>? albums =
|
||||||
|
await _albumService.getAlbums(isShared: false);
|
||||||
|
|
||||||
|
if (albums != null) {
|
||||||
|
state = albums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAlbum(String albumId) {
|
||||||
|
state = state.where((album) => album.id != albumId).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AlbumResponseDto?> createAlbum(
|
||||||
|
String albumTitle,
|
||||||
|
Set<AssetResponseDto> assets,
|
||||||
|
) async {
|
||||||
|
AlbumResponseDto? album =
|
||||||
|
await _albumService.createAlbum(albumTitle, assets, []);
|
||||||
|
|
||||||
|
if (album != null) {
|
||||||
|
state = [...state, album];
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final albumProvider =
|
||||||
|
StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
|
||||||
|
return AlbumNotifier(ref.watch(albumServiceProvider));
|
||||||
|
});
|
||||||
@@ -13,4 +13,5 @@ class AlbumTitleNotifier extends StateNotifier<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
|
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
|
||||||
(ref) => AlbumTitleNotifier());
|
(ref) => AlbumTitleNotifier(),
|
||||||
|
);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/album_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/album/models/album_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
|
||||||
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||||
AlbumViewerNotifier(this.ref)
|
AlbumViewerNotifier(this.ref)
|
||||||
@@ -30,8 +30,11 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> changeAlbumTitle(
|
Future<bool> changeAlbumTitle(
|
||||||
String albumId, String ownerId, String newAlbumTitle) async {
|
String albumId,
|
||||||
SharedAlbumService service = ref.watch(sharedAlbumServiceProvider);
|
String ownerId,
|
||||||
|
String newAlbumTitle,
|
||||||
|
) async {
|
||||||
|
AlbumService service = ref.watch(albumServiceProvider);
|
||||||
|
|
||||||
bool isSuccess =
|
bool isSuccess =
|
||||||
await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
|
await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
|
||||||
@@ -1,41 +1,46 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/asset_selection_state.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
AssetSelectionNotifier()
|
AssetSelectionNotifier()
|
||||||
: super(AssetSelectionState(
|
: super(
|
||||||
selectedNewAssetsForAlbum: {},
|
AssetSelectionState(
|
||||||
selectedMonths: {},
|
selectedNewAssetsForAlbum: {},
|
||||||
selectedAdditionalAssetsForAlbum: {},
|
selectedMonths: {},
|
||||||
selectedAssetsInAlbumViewer: {},
|
selectedAdditionalAssetsForAlbum: {},
|
||||||
isAlbumExist: false,
|
selectedAssetsInAlbumViewer: {},
|
||||||
isMultiselectEnable: false,
|
isAlbumExist: false,
|
||||||
));
|
isMultiselectEnable: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
void setIsAlbumExist(bool isAlbumExist) {
|
void setIsAlbumExist(bool isAlbumExist) {
|
||||||
state = state.copyWith(isAlbumExist: isAlbumExist);
|
state = state.copyWith(isAlbumExist: isAlbumExist);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAssetsInMonth(
|
void removeAssetsInMonth(
|
||||||
String removedMonth, List<ImmichAsset> assetsInMonth) {
|
String removedMonth,
|
||||||
Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum;
|
List<AssetResponseDto> assetsInMonth,
|
||||||
|
) {
|
||||||
|
Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum;
|
||||||
Set<String> currentMonthList = state.selectedMonths;
|
Set<String> currentMonthList = state.selectedMonths;
|
||||||
|
|
||||||
currentMonthList
|
currentMonthList
|
||||||
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
||||||
|
|
||||||
for (ImmichAsset asset in assetsInMonth) {
|
for (AssetResponseDto asset in assetsInMonth) {
|
||||||
currentAssetList.removeWhere((e) => e.id == asset.id);
|
currentAssetList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedNewAssetsForAlbum: currentAssetList,
|
selectedNewAssetsForAlbum: currentAssetList,
|
||||||
selectedMonths: currentMonthList);
|
selectedMonths: currentMonthList,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAdditionalAssets(List<ImmichAsset> assets) {
|
void addAdditionalAssets(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAdditionalAssetsForAlbum: {
|
selectedAdditionalAssetsForAlbum: {
|
||||||
...state.selectedAdditionalAssetsForAlbum,
|
...state.selectedAdditionalAssetsForAlbum,
|
||||||
@@ -44,7 +49,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) {
|
void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedMonths: {...state.selectedMonths, month},
|
selectedMonths: {...state.selectedMonths, month},
|
||||||
selectedNewAssetsForAlbum: {
|
selectedNewAssetsForAlbum: {
|
||||||
@@ -54,7 +59,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addNewAssets(List<ImmichAsset> assets) {
|
void addNewAssets(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedNewAssetsForAlbum: {
|
selectedNewAssetsForAlbum: {
|
||||||
...state.selectedNewAssetsForAlbum,
|
...state.selectedNewAssetsForAlbum,
|
||||||
@@ -63,20 +68,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedNewAssets(List<ImmichAsset> assets) {
|
void removeSelectedNewAssets(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedNewAssetsForAlbum;
|
Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedAdditionalAssets(List<ImmichAsset> assets) {
|
void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedAdditionalAssetsForAlbum;
|
Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +109,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAssetsInAlbumViewer(List<ImmichAsset> assets) {
|
void addAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAssetsInAlbumViewer: {
|
selectedAssetsInAlbumViewer: {
|
||||||
...state.selectedAssetsInAlbumViewer,
|
...state.selectedAssetsInAlbumViewer,
|
||||||
@@ -113,10 +118,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAssetsInAlbumViewer(List<ImmichAsset> assets) {
|
void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedAssetsInAlbumViewer;
|
Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
|
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
|
||||||
|
|
||||||
|
final AlbumService _sharedAlbumService;
|
||||||
|
|
||||||
|
Future<AlbumResponseDto?> createSharedAlbum(
|
||||||
|
String albumName,
|
||||||
|
Set<AssetResponseDto> assets,
|
||||||
|
List<String> sharedUserIds,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
var newAlbum = await _sharedAlbumService.createAlbum(
|
||||||
|
albumName,
|
||||||
|
assets,
|
||||||
|
sharedUserIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newAlbum != null) {
|
||||||
|
state = [...state, newAlbum];
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAlbum;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error createSharedAlbum ${e.toString()}");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllSharedAlbums() async {
|
||||||
|
List<AlbumResponseDto>? sharedAlbums =
|
||||||
|
await _sharedAlbumService.getAlbums(isShared: true);
|
||||||
|
|
||||||
|
if (sharedAlbums != null) {
|
||||||
|
state = sharedAlbums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAlbum(String albumId) async {
|
||||||
|
state = state.where((album) => album.id != albumId).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> leaveAlbum(String albumId) async {
|
||||||
|
var res = await _sharedAlbumService.leaveAlbum(albumId);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
state = state.where((album) => album.id != albumId).toList();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> removeAssetFromAlbum(
|
||||||
|
String albumId,
|
||||||
|
List<String> assetIds,
|
||||||
|
) async {
|
||||||
|
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final sharedAlbumProvider =
|
||||||
|
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
|
||||||
|
return SharedAlbumNotifier(ref.watch(albumServiceProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
||||||
|
.family<AlbumResponseDto?, String>((ref, albumId) async {
|
||||||
|
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
|
||||||
|
|
||||||
|
return await sharedAlbumService.getAlbumDetail(albumId);
|
||||||
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/user.service.dart';
|
import 'package:immich_mobile/shared/services/user.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final suggestedSharedUsersProvider =
|
final suggestedSharedUsersProvider =
|
||||||
FutureProvider.autoDispose<List<User>>((ref) async {
|
FutureProvider.autoDispose<List<UserResponseDto>>((ref) async {
|
||||||
UserService userService = ref.watch(userServiceProvider);
|
UserService userService = ref.watch(userServiceProvider);
|
||||||
|
|
||||||
return await userService.getAllUsersInfo();
|
return await userService.getAllUsersInfo(isAll: false) ?? [];
|
||||||
});
|
});
|
||||||
148
mobile/lib/modules/album/services/album.service.dart
Normal file
148
mobile/lib/modules/album/services/album.service.dart
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final albumServiceProvider = Provider(
|
||||||
|
(ref) => AlbumService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class AlbumService {
|
||||||
|
final ApiService _apiService;
|
||||||
|
|
||||||
|
AlbumService(this._apiService);
|
||||||
|
|
||||||
|
Future<List<AlbumResponseDto>?> getAlbums({required bool isShared}) async {
|
||||||
|
try {
|
||||||
|
return await _apiService.albumApi
|
||||||
|
.getAllAlbums(shared: isShared ? isShared : null);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error getAllSharedAlbum ${e.toString()}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AlbumResponseDto?> createAlbum(
|
||||||
|
String albumName,
|
||||||
|
Set<AssetResponseDto> assets,
|
||||||
|
List<String> sharedUserIds,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
return await _apiService.albumApi.createAlbum(
|
||||||
|
CreateAlbumDto(
|
||||||
|
albumName: albumName,
|
||||||
|
assetIds: assets.map((asset) => asset.id).toList(),
|
||||||
|
sharedWithUserIds: sharedUserIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error createSharedAlbum ${e.toString()}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AlbumResponseDto?> getAlbumDetail(String albumId) async {
|
||||||
|
try {
|
||||||
|
return await _apiService.albumApi.getAlbumInfo(albumId);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error [getAlbumDetail] ${e.toString()}');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addAdditionalAssetToAlbum(
|
||||||
|
Set<AssetResponseDto> assets,
|
||||||
|
String albumId,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
var result = await _apiService.albumApi.addAssetsToAlbum(
|
||||||
|
albumId,
|
||||||
|
AddAssetsDto(assetIds: assets.map((asset) => asset.id).toList()),
|
||||||
|
);
|
||||||
|
return result != null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addAdditionalUserToAlbum(
|
||||||
|
List<String> sharedUserIds,
|
||||||
|
String albumId,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
var result = await _apiService.albumApi.addUsersToAlbum(
|
||||||
|
albumId,
|
||||||
|
AddUsersDto(sharedUserIds: sharedUserIds),
|
||||||
|
);
|
||||||
|
|
||||||
|
return result != null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteAlbum(String albumId) async {
|
||||||
|
try {
|
||||||
|
await _apiService.albumApi.deleteAlbum(albumId);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> leaveAlbum(String albumId) async {
|
||||||
|
try {
|
||||||
|
await _apiService.albumApi.removeUserFromAlbum(albumId, "me");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> removeAssetFromAlbum(
|
||||||
|
String albumId,
|
||||||
|
List<String> assetIds,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await _apiService.albumApi.removeAssetFromAlbum(
|
||||||
|
albumId,
|
||||||
|
RemoveAssetsDto(assetIds: assetIds),
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> changeTitleAlbum(
|
||||||
|
String albumId,
|
||||||
|
String ownerId,
|
||||||
|
String newAlbumTitle,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await _apiService.albumApi.updateAlbumInfo(
|
||||||
|
albumId,
|
||||||
|
UpdateAlbumDto(
|
||||||
|
albumName: newAlbumTitle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
final String labelText;
|
final String labelText;
|
||||||
final IconData iconData;
|
final IconData iconData;
|
||||||
|
|
||||||
const AlbumActionOutlinedButton(
|
const AlbumActionOutlinedButton({
|
||||||
{Key? key,
|
Key? key,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
required this.labelText,
|
required this.labelText,
|
||||||
required this.iconData})
|
required this.iconData,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -31,7 +31,10 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
label: Text(
|
label: Text(
|
||||||
labelText,
|
labelText,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
77
mobile/lib/modules/album/ui/album_thumbnail_card.dart
Normal file
77
mobile/lib/modules/album/ui/album_thumbnail_card.dart
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:transparent_image/transparent_image.dart';
|
||||||
|
|
||||||
|
class AlbumThumbnailCard extends StatelessWidget {
|
||||||
|
const AlbumThumbnailCard({Key? key, required this.album}) : super(key: key);
|
||||||
|
|
||||||
|
final AlbumResponseDto album;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 32.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: FadeInImage(
|
||||||
|
width: MediaQuery.of(context).size.width / 2 - 18,
|
||||||
|
height: MediaQuery.of(context).size.width / 2 - 18,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: MemoryImage(kTransparentImage),
|
||||||
|
image: NetworkImage(
|
||||||
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${album.albumThumbnailAssetId}?format=JPEG',
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
||||||
|
},
|
||||||
|
),
|
||||||
|
fadeInDuration: const Duration(milliseconds: 200),
|
||||||
|
fadeOutDuration: const Duration(milliseconds: 200),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
album.albumName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${album.assets.length} item${album.assets.length > 1 ? 's' : ''}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (album.shared)
|
||||||
|
const Text(
|
||||||
|
' · Shared',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
||||||
|
|
||||||
class AlbumTitleTextField extends ConsumerWidget {
|
class AlbumTitleTextField extends ConsumerWidget {
|
||||||
const AlbumTitleTextField({
|
const AlbumTitleTextField({
|
||||||
@@ -30,7 +31,10 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
focusNode: albumTitleTextFieldFocusNode,
|
focusNode: albumTitleTextFieldFocusNode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold),
|
fontSize: 28,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
controller: albumTitleController,
|
controller: albumTitleController,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
isAlbumTitleTextFieldFocus.value = true;
|
isAlbumTitleTextFieldFocus.value = true;
|
||||||
@@ -59,7 +63,7 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
borderSide: const BorderSide(color: Colors.transparent),
|
borderSide: const BorderSide(color: Colors.transparent),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
hintText: 'Add a title',
|
hintText: 'share_add_title'.tr(),
|
||||||
focusColor: Colors.grey[300],
|
focusColor: Colors.grey[300],
|
||||||
fillColor: Colors.grey[200],
|
fillColor: Colors.grey[200],
|
||||||
filled: isAlbumTitleTextFieldFocus.value,
|
filled: isAlbumTitleTextFieldFocus.value,
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
||||||
const AlbumViewerAppbar({
|
const AlbumViewerAppbar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required AsyncValue<SharedAlbum> albumInfo,
|
required this.albumInfo,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.albumId,
|
required this.albumId,
|
||||||
}) : _albumInfo = albumInfo,
|
}) : super(key: key);
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
final AsyncValue<SharedAlbum> _albumInfo;
|
final AlbumResponseDto albumInfo;
|
||||||
final String userId;
|
final String userId;
|
||||||
final String albumId;
|
final String albumId;
|
||||||
|
|
||||||
@@ -37,15 +39,22 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
bool isSuccess =
|
bool isSuccess =
|
||||||
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
|
await ref.watch(albumServiceProvider).deleteAlbum(albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
AutoRouter.of(context)
|
if (albumInfo.shared) {
|
||||||
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
|
||||||
|
AutoRouter.of(context)
|
||||||
|
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
|
} else {
|
||||||
|
ref.watch(albumProvider.notifier).deleteAlbum(albumId);
|
||||||
|
AutoRouter.of(context)
|
||||||
|
.navigate(const TabControllerRoute(children: [LibraryRoute()]));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Failed to delete album",
|
msg: "album_viewer_appbar_share_err_delete".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -67,7 +76,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Failed to leave album",
|
msg: "album_viewer_appbar_share_err_leave".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -93,7 +102,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "There are problems in removing assets from album",
|
msg: "album_viewer_appbar_share_err_remove".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -104,35 +113,35 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
|
|
||||||
_buildBottomSheetActionButton() {
|
_buildBottomSheetActionButton() {
|
||||||
if (isMultiSelectionEnable) {
|
if (isMultiSelectionEnable) {
|
||||||
if (_albumInfo.asData?.value.ownerId == userId) {
|
if (albumInfo.ownerId == userId) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.delete_sweep_rounded),
|
leading: const Icon(Icons.delete_sweep_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Remove from album',
|
'album_viewer_appbar_share_remove',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
onTap: () => _onRemoveFromAlbumPressed(albumId),
|
onTap: () => _onRemoveFromAlbumPressed(albumId),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_albumInfo.asData?.value.ownerId == userId) {
|
if (albumInfo.ownerId == userId) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.delete_forever_rounded),
|
leading: const Icon(Icons.delete_forever_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Delete album',
|
'album_viewer_appbar_share_delete',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
onTap: () => _onDeleteAlbumPressed(albumId),
|
onTap: () => _onDeleteAlbumPressed(albumId),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.person_remove_rounded),
|
leading: const Icon(Icons.person_remove_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Leave album',
|
'album_viewer_appbar_share_leave',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
onTap: () => _onLeaveAlbumPressed(albumId),
|
onTap: () => _onLeaveAlbumPressed(albumId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -176,7 +185,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Failed to change album title",
|
msg: "album_viewer_appbar_share_err_title".tr(),
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
);
|
);
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||||
final SharedAlbum albumInfo;
|
final AlbumResponseDto albumInfo;
|
||||||
final FocusNode titleFocusNode;
|
final FocusNode titleFocusNode;
|
||||||
const AlbumViewerEditableTitle(
|
const AlbumViewerEditableTitle({
|
||||||
{Key? key, required this.albumInfo, required this.titleFocusNode})
|
Key? key,
|
||||||
: super(key: key);
|
required this.albumInfo,
|
||||||
|
required this.titleFocusNode,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -23,12 +26,15 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
titleFocusNode.addListener(onFocusModeChange);
|
() {
|
||||||
return () {
|
titleFocusNode.addListener(onFocusModeChange);
|
||||||
titleFocusNode.removeListener(onFocusModeChange);
|
return () {
|
||||||
};
|
titleFocusNode.removeListener(onFocusModeChange);
|
||||||
}, []);
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return TextField(
|
return TextField(
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -74,7 +80,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
focusColor: Colors.grey[300],
|
focusColor: Colors.grey[300],
|
||||||
fillColor: Colors.grey[200],
|
fillColor: Colors.grey[200],
|
||||||
filled: titleFocusNode.hasFocus,
|
filled: titleFocusNode.hasFocus,
|
||||||
hintText: 'Add a title',
|
hintText: 'share_add_title'.tr(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6,21 +6,26 @@ 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/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerThumbnail extends HookConsumerWidget {
|
class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
final List<AssetResponseDto> assetList;
|
||||||
|
|
||||||
const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key);
|
const AlbumViewerThumbnail({
|
||||||
|
Key? key,
|
||||||
|
required this.asset,
|
||||||
|
required this.assetList,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final cacheKey = useState(1);
|
final cacheKey = useState(1);
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
final selectedAssetsInAlbumViewer =
|
final selectedAssetsInAlbumViewer =
|
||||||
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
@@ -28,24 +33,13 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
ref.watch(assetSelectionProvider).isMultiselectEnable;
|
ref.watch(assetSelectionProvider).isMultiselectEnable;
|
||||||
|
|
||||||
_viewAsset() {
|
_viewAsset() {
|
||||||
if (asset.type == 'IMAGE') {
|
AutoRouter.of(context).push(
|
||||||
AutoRouter.of(context).push(
|
GalleryViewerRoute(
|
||||||
ImageViewerRoute(
|
asset: asset,
|
||||||
imageUrl:
|
assetList: assetList,
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
|
thumbnailRequestUrl: thumbnailRequestUrl,
|
||||||
heroTag: asset.id,
|
),
|
||||||
thumbnailUrl: thumbnailRequestUrl,
|
);
|
||||||
asset: asset,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
AutoRouter.of(context).push(
|
|
||||||
VideoViewerRoute(
|
|
||||||
videoUrl:
|
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
|
||||||
asset: asset),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BoxBorder drawBorderColor() {
|
BoxBorder drawBorderColor() {
|
||||||
@@ -170,16 +164,13 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset,
|
onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset,
|
||||||
onLongPress: _enableMultiSelection,
|
onLongPress: _enableMultiSelection,
|
||||||
child: Hero(
|
child: Stack(
|
||||||
tag: asset.id,
|
children: [
|
||||||
child: Stack(
|
_buildThumbnailImage(),
|
||||||
children: [
|
_buildAssetStoreLocationIcon(),
|
||||||
_buildThumbnailImage(),
|
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
|
||||||
_buildAssetStoreLocationIcon(),
|
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
||||||
if (asset.type != 'IMAGE') _buildVideoLabel(),
|
],
|
||||||
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/selection_thumbnail_image.dart';
|
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetGridByMonth extends HookConsumerWidget {
|
class AssetGridByMonth extends HookConsumerWidget {
|
||||||
final List<ImmichAsset> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
const AssetGridByMonth({Key? key, required this.assetGroup})
|
const AssetGridByMonth({Key? key, required this.assetGroup})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@override
|
@override
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class MonthGroupTitle extends HookConsumerWidget {
|
class MonthGroupTitle extends HookConsumerWidget {
|
||||||
final String month;
|
final String month;
|
||||||
final List<ImmichAsset> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
|
|
||||||
const MonthGroupTitle(
|
const MonthGroupTitle({
|
||||||
{Key? key, required this.month, required this.assetGroup})
|
Key? key,
|
||||||
: super(key: key);
|
required this.month,
|
||||||
|
required this.assetGroup,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -75,7 +77,11 @@ class MonthGroupTitle extends HookConsumerWidget {
|
|||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 29.0, bottom: 29.0, left: 14.0, right: 8.0),
|
top: 29.0,
|
||||||
|
bottom: 29.0,
|
||||||
|
left: 14.0,
|
||||||
|
right: 8.0,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
@@ -90,13 +96,16 @@ class MonthGroupTitle extends HookConsumerWidget {
|
|||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
GestureDetector(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
onTap: _handleTitleIconClick,
|
||||||
child: Text(
|
child: Padding(
|
||||||
_getSimplifiedMonth(),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontSize: 24,
|
_getSimplifiedMonth(),
|
||||||
color: Theme.of(context).primaryColor,
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -4,11 +4,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SelectionThumbnailImage extends HookConsumerWidget {
|
class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
const SelectionThumbnailImage({Key? key, required this.asset})
|
const SelectionThumbnailImage({Key? key, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -18,25 +18,29 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
final cacheKey = useState(1);
|
final cacheKey = useState(1);
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
||||||
var selectedAsset =
|
var selectedAsset =
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||||
var newAssetsForAlbum =
|
var newAssetsForAlbum =
|
||||||
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
||||||
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||||
|
|
||||||
Widget _buildSelectionIcon(ImmichAsset asset) {
|
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
||||||
if (selectedAsset.contains(asset) && !isAlbumExist) {
|
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
|
var isNewlySelected =
|
||||||
|
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||||
|
|
||||||
|
if (isSelected && !isAlbumExist) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
);
|
);
|
||||||
} else if (selectedAsset.contains(asset) && isAlbumExist) {
|
} else if (isSelected && isAlbumExist) {
|
||||||
return const Icon(
|
return const Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: Color.fromARGB(255, 233, 233, 233),
|
color: Color.fromARGB(255, 233, 233, 233),
|
||||||
);
|
);
|
||||||
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) {
|
} else if (isNewlySelected && isAlbumExist) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
@@ -50,17 +54,21 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BoxBorder drawBorderColor() {
|
BoxBorder drawBorderColor() {
|
||||||
if (selectedAsset.contains(asset) && !isAlbumExist) {
|
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
|
var isNewlySelected =
|
||||||
|
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||||
|
|
||||||
|
if (isSelected && !isAlbumExist) {
|
||||||
return Border.all(
|
return Border.all(
|
||||||
color: Theme.of(context).primaryColorLight,
|
color: Theme.of(context).primaryColorLight,
|
||||||
width: 10,
|
width: 10,
|
||||||
);
|
);
|
||||||
} else if (selectedAsset.contains(asset) && isAlbumExist) {
|
} else if (isSelected && isAlbumExist) {
|
||||||
return Border.all(
|
return Border.all(
|
||||||
color: const Color.fromARGB(255, 190, 190, 190),
|
color: const Color.fromARGB(255, 190, 190, 190),
|
||||||
width: 10,
|
width: 10,
|
||||||
);
|
);
|
||||||
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) {
|
} else if (isNewlySelected && isAlbumExist) {
|
||||||
return Border.all(
|
return Border.all(
|
||||||
color: Theme.of(context).primaryColorLight,
|
color: Theme.of(context).primaryColorLight,
|
||||||
width: 10,
|
width: 10,
|
||||||
@@ -71,10 +79,15 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
var isSelected =
|
||||||
|
selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
|
var isNewlySelected =
|
||||||
|
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||||
|
|
||||||
if (isAlbumExist) {
|
if (isAlbumExist) {
|
||||||
// Operation for existing album
|
// Operation for existing album
|
||||||
if (!selectedAsset.contains(asset)) {
|
if (!isSelected) {
|
||||||
if (newAssetsForAlbum.contains(asset)) {
|
if (isNewlySelected) {
|
||||||
ref
|
ref
|
||||||
.watch(assetSelectionProvider.notifier)
|
.watch(assetSelectionProvider.notifier)
|
||||||
.removeSelectedAdditionalAssets([asset]);
|
.removeSelectedAdditionalAssets([asset]);
|
||||||
@@ -86,7 +99,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Operation for new album
|
// Operation for new album
|
||||||
if (selectedAsset.contains(asset)) {
|
if (isSelected) {
|
||||||
ref
|
ref
|
||||||
.watch(assetSelectionProvider.notifier)
|
.watch(assetSelectionProvider.notifier)
|
||||||
.removeSelectedNewAssets([asset]);
|
.removeSelectedNewAssets([asset]);
|
||||||
@@ -103,7 +116,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||||
width: 150,
|
width: 150,
|
||||||
height: 150,
|
height: 150,
|
||||||
memCacheHeight: asset.type == 'IMAGE' ? 150 : 150,
|
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
@@ -131,14 +144,14 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
child: _buildSelectionIcon(asset),
|
child: _buildSelectionIcon(asset),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (asset.type != 'IMAGE')
|
if (asset.type != AssetTypeEnum.IMAGE)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
right: 5,
|
right: 5,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${asset.duration?.substring(0, 7)}',
|
asset.duration.substring(0, 7),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
@@ -4,10 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
const SharedAlbumThumbnailImage({Key? key, required this.asset})
|
const SharedAlbumThumbnailImage({Key? key, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -18,7 +18,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
|||||||
|
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -30,7 +30,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
|||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 500,
|
height: 500,
|
||||||
memCacheHeight: asset.type == 'IMAGE' ? 500 : 500,
|
memCacheHeight: 500,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
@@ -15,8 +16,6 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
pinned: true,
|
pinned: true,
|
||||||
snap: false,
|
snap: false,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
// leading: Container(),
|
|
||||||
// elevation: 0,
|
|
||||||
title: Text(
|
title: Text(
|
||||||
'IMMICH',
|
'IMMICH',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -39,22 +38,23 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: MaterialStateProperty.all(
|
backgroundColor: MaterialStateProperty.all(
|
||||||
Theme.of(context).primaryColor.withAlpha(20)),
|
Theme.of(context).primaryColor.withAlpha(20),
|
||||||
|
),
|
||||||
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context)
|
AutoRouter.of(context)
|
||||||
.push(const CreateSharedAlbumRoute());
|
.push(CreateAlbumRoute(isSharedAlbum: true));
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.photo_album_outlined,
|
Icons.photo_album_outlined,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"Create shared album",
|
"sharing_silver_appbar_create_shared_album",
|
||||||
style:
|
style:
|
||||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -64,7 +64,8 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: MaterialStateProperty.all(
|
backgroundColor: MaterialStateProperty.all(
|
||||||
Theme.of(context).primaryColor.withAlpha(20)),
|
Theme.of(context).primaryColor.withAlpha(20),
|
||||||
|
),
|
||||||
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||||
),
|
),
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
@@ -73,10 +74,10 @@ class SharingSliverAppBar extends StatelessWidget {
|
|||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"Share with partner",
|
"sharing_silver_appbar_share_partner",
|
||||||
style:
|
style:
|
||||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_action_outlined_button.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_viewer_appbar.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_viewer_editable_title.dart';
|
import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_viewer_thumbnail.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerPage extends HookConsumerWidget {
|
class AlbumViewerPage extends HookConsumerWidget {
|
||||||
final String albumId;
|
final String albumId;
|
||||||
@@ -29,18 +29,17 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
FocusNode titleFocusNode = useFocusNode();
|
FocusNode titleFocusNode = useFocusNode();
|
||||||
ScrollController scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
AsyncValue<SharedAlbum> albumInfo =
|
var albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
|
||||||
ref.watch(sharedAlbumDetailProvider(albumId));
|
|
||||||
|
|
||||||
final userId = ref.watch(authenticationProvider).userId;
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
|
|
||||||
/// Find out if the assets in album exist on the device
|
/// Find out if the assets in album exist on the device
|
||||||
/// If they exist, add to selected asset state to show they are already selected.
|
/// If they exist, add to selected asset state to show they are already selected.
|
||||||
void _onAddPhotosPressed(SharedAlbum albumInfo) async {
|
void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
|
||||||
if (albumInfo.assets?.isNotEmpty == true) {
|
if (albumInfo.assets.isNotEmpty == true) {
|
||||||
ref
|
ref
|
||||||
.watch(assetSelectionProvider.notifier)
|
.watch(assetSelectionProvider.notifier)
|
||||||
.addNewAssets(albumInfo.assets!.toList());
|
.addNewAssets(albumInfo.assets.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
||||||
@@ -53,10 +52,11 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
|
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
var isSuccess = await ref
|
var isSuccess =
|
||||||
.watch(sharedAlbumServiceProvider)
|
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
||||||
.addAdditionalAssetToAlbum(
|
returnPayload.selectedAdditionalAsset,
|
||||||
returnPayload.selectedAdditionalAsset, albumId);
|
albumId,
|
||||||
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
ref.refresh(sharedAlbumDetailProvider(albumId));
|
ref.refresh(sharedAlbumDetailProvider(albumId));
|
||||||
@@ -71,16 +71,17 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAddUsersPressed(SharedAlbum albumInfo) async {
|
void _onAddUsersPressed(AlbumResponseDto albumInfo) async {
|
||||||
List<String>? sharedUserIds = await AutoRouter.of(context)
|
List<String>? sharedUserIds =
|
||||||
.push<List<String>?>(
|
await AutoRouter.of(context).push<List<String>?>(
|
||||||
SelectAdditionalUserForSharingRoute(albumInfo: albumInfo));
|
SelectAdditionalUserForSharingRoute(albumInfo: albumInfo),
|
||||||
|
);
|
||||||
|
|
||||||
if (sharedUserIds != null) {
|
if (sharedUserIds != null) {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
var isSuccess = await ref
|
var isSuccess = await ref
|
||||||
.watch(sharedAlbumServiceProvider)
|
.watch(albumServiceProvider)
|
||||||
.addAdditionalUserToAlbum(sharedUserIds, albumId);
|
.addAdditionalUserToAlbum(sharedUserIds, albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
@@ -91,7 +92,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTitle(SharedAlbum albumInfo) {
|
Widget _buildTitle(AlbumResponseDto albumInfo) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
|
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
|
||||||
child: userId == albumInfo.ownerId
|
child: userId == albumInfo.ownerId
|
||||||
@@ -101,19 +102,24 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Text(albumInfo.albumName,
|
child: Text(
|
||||||
style: const TextStyle(
|
albumInfo.albumName,
|
||||||
fontSize: 24, fontWeight: FontWeight.bold)),
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAlbumDateRange(SharedAlbum albumInfo) {
|
Widget _buildAlbumDateRange(AlbumResponseDto albumInfo) {
|
||||||
String startDate = "";
|
String startDate = "";
|
||||||
DateTime parsedStartDate =
|
DateTime parsedStartDate =
|
||||||
DateTime.parse(albumInfo.assets!.first.createdAt);
|
DateTime.parse(albumInfo.assets.first.createdAt);
|
||||||
DateTime parsedEndDate = DateTime.parse(
|
DateTime parsedEndDate = DateTime.parse(
|
||||||
albumInfo.assets?.last.createdAt ?? '11111111'); //Need default.
|
albumInfo.assets.last.createdAt,
|
||||||
|
); //Need default.
|
||||||
|
|
||||||
if (parsedStartDate.year == parsedEndDate.year) {
|
if (parsedStartDate.year == parsedEndDate.year) {
|
||||||
startDate = DateFormat('LLL d').format(parsedStartDate);
|
startDate = DateFormat('LLL d').format(parsedStartDate);
|
||||||
@@ -124,55 +130,64 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
String endDate = DateFormat('LLL d, y').format(parsedEndDate);
|
String endDate = DateFormat('LLL d, y').format(parsedEndDate);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 8),
|
padding: EdgeInsets.only(
|
||||||
|
left: 16.0,
|
||||||
|
top: 8.0,
|
||||||
|
bottom: albumInfo.shared ? 0.0 : 8.0,
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
"$startDate-$endDate",
|
"$startDate-$endDate",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(SharedAlbum albumInfo) {
|
Widget _buildHeader(AlbumResponseDto albumInfo) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildTitle(albumInfo),
|
_buildTitle(albumInfo),
|
||||||
if (albumInfo.assets?.isNotEmpty == true)
|
if (albumInfo.assets.isNotEmpty == true)
|
||||||
_buildAlbumDateRange(albumInfo),
|
_buildAlbumDateRange(albumInfo),
|
||||||
SizedBox(
|
if (albumInfo.shared)
|
||||||
height: 60,
|
SizedBox(
|
||||||
child: ListView.builder(
|
height: 60,
|
||||||
padding: const EdgeInsets.only(left: 16),
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
padding: const EdgeInsets.only(left: 16),
|
||||||
itemBuilder: ((context, index) {
|
scrollDirection: Axis.horizontal,
|
||||||
return Padding(
|
itemBuilder: ((context, index) {
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
return Padding(
|
||||||
child: CircleAvatar(
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
backgroundColor: Colors.grey[300],
|
child: CircleAvatar(
|
||||||
radius: 18,
|
backgroundColor: Colors.grey[300],
|
||||||
child: Padding(
|
radius: 18,
|
||||||
padding: const EdgeInsets.all(2.0),
|
child: Padding(
|
||||||
child: ClipRRect(
|
padding: const EdgeInsets.all(2.0),
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
child: ClipRRect(
|
||||||
child:
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
Image.asset('assets/immich-logo-no-outline.png'),
|
child: Image.asset(
|
||||||
|
'assets/immich-logo-no-outline.png',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
itemCount: albumInfo.sharedUsers.length,
|
||||||
itemCount: albumInfo.sharedUsers.length,
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImageGrid(SharedAlbum albumInfo) {
|
Widget _buildImageGrid(AlbumResponseDto albumInfo) {
|
||||||
if (albumInfo.assets?.isNotEmpty == true) {
|
if (albumInfo.assets.isNotEmpty) {
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
sliver: SliverGrid(
|
sliver: SliverGrid(
|
||||||
@@ -183,9 +198,12 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return AlbumViewerThumbnail(asset: albumInfo.assets![index]);
|
return AlbumViewerThumbnail(
|
||||||
|
asset: albumInfo.assets[index],
|
||||||
|
assetList: albumInfo.assets,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
childCount: albumInfo.assets?.length,
|
childCount: albumInfo.assets.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -193,7 +211,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
return const SliverToBoxAdapter();
|
return const SliverToBoxAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildControlButton(SharedAlbum albumInfo) {
|
Widget _buildControlButton(AlbumResponseDto albumInfo) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
|
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -204,13 +222,13 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
AlbumActionOutlinedButton(
|
AlbumActionOutlinedButton(
|
||||||
iconData: Icons.add_photo_alternate_outlined,
|
iconData: Icons.add_photo_alternate_outlined,
|
||||||
onPressed: () => _onAddPhotosPressed(albumInfo),
|
onPressed: () => _onAddPhotosPressed(albumInfo),
|
||||||
labelText: "Add photos",
|
labelText: "share_add_photos".tr(),
|
||||||
),
|
),
|
||||||
if (userId == albumInfo.ownerId)
|
if (userId == albumInfo.ownerId)
|
||||||
AlbumActionOutlinedButton(
|
AlbumActionOutlinedButton(
|
||||||
iconData: Icons.person_add_alt_rounded,
|
iconData: Icons.person_add_alt_rounded,
|
||||||
onPressed: () => _onAddUsersPressed(albumInfo),
|
onPressed: () => _onAddUsersPressed(albumInfo),
|
||||||
labelText: "Add users",
|
labelText: "album_viewer_page_share_add_users".tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -218,7 +236,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(SharedAlbum albumInfo) {
|
Widget _buildBody(AlbumResponseDto albumInfo) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
titleFocusNode.unfocus();
|
titleFocusNode.unfocus();
|
||||||
@@ -250,10 +268,26 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AlbumViewerAppbar(
|
appBar: albumInfo.when(
|
||||||
albumInfo: albumInfo, userId: userId, albumId: albumId),
|
data: (AlbumResponseDto? data) {
|
||||||
|
if (data != null) {
|
||||||
|
return AlbumViewerAppbar(
|
||||||
|
albumInfo: data,
|
||||||
|
userId: userId,
|
||||||
|
albumId: albumId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
error: (e, _) => null,
|
||||||
|
loading: () => null,
|
||||||
|
),
|
||||||
body: albumInfo.when(
|
body: albumInfo.when(
|
||||||
data: (albumInfo) => _buildBody(albumInfo),
|
data: (albumInfo) => albumInfo != null
|
||||||
|
? _buildBody(albumInfo)
|
||||||
|
: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
error: (e, _) => Center(child: Text("Error loading album info $e")),
|
error: (e, _) => Center(child: Text("Error loading album info $e")),
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: ImmichLoadingIndicator(),
|
child: ImmichLoadingIndicator(),
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/asset_grid_by_month.dart';
|
import 'package:immich_mobile/modules/album/ui/asset_grid_by_month.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/month_group_title.dart';
|
import 'package:immich_mobile/modules/album/ui/month_group_title.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
||||||
|
|
||||||
class AssetSelectionPage extends HookConsumerWidget {
|
class AssetSelectionPage extends HookConsumerWidget {
|
||||||
const AssetSelectionPage({Key? key}) : super(key: key);
|
const AssetSelectionPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
ScrollController scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
@@ -65,9 +67,9 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
title: selectedAssets.isEmpty
|
title: selectedAssets.isEmpty
|
||||||
? const Text(
|
? const Text(
|
||||||
'Add photos',
|
'share_add_photos',
|
||||||
style: TextStyle(fontSize: 18),
|
style: TextStyle(fontSize: 18),
|
||||||
)
|
).tr()
|
||||||
: Text(
|
: Text(
|
||||||
_buildAssetCountText(),
|
_buildAssetCountText(),
|
||||||
style: const TextStyle(fontSize: 18),
|
style: const TextStyle(fontSize: 18),
|
||||||
@@ -86,9 +88,9 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
AutoRouter.of(context).pop(payload);
|
AutoRouter.of(context).pop(payload);
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"Add",
|
"share_add",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
258
mobile/lib/modules/album/views/create_album_page.dart
Normal file
258
mobile/lib/modules/album/views/create_album_page.dart
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class CreateAlbumPage extends HookConsumerWidget {
|
||||||
|
bool isSharedAlbum;
|
||||||
|
|
||||||
|
CreateAlbumPage({Key? key, required this.isSharedAlbum}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albumTitleController =
|
||||||
|
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
|
final albumTitleTextFieldFocusNode = useFocusNode();
|
||||||
|
final isAlbumTitleTextFieldFocus = useState(false);
|
||||||
|
final isAlbumTitleEmpty = useState(true);
|
||||||
|
final selectedAssets =
|
||||||
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||||
|
|
||||||
|
_showSelectUserPage() {
|
||||||
|
AutoRouter.of(context).push(const SelectUserForSharingRoute());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onBackgroundTapped() {
|
||||||
|
albumTitleTextFieldFocusNode.unfocus();
|
||||||
|
isAlbumTitleTextFieldFocus.value = false;
|
||||||
|
|
||||||
|
if (albumTitleController.text.isEmpty) {
|
||||||
|
albumTitleController.text = 'Untitled';
|
||||||
|
ref.watch(albumTitleProvider.notifier).setAlbumTitle('Untitled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSelectPhotosButtonPressed() async {
|
||||||
|
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false);
|
||||||
|
|
||||||
|
AssetSelectionPageResult? selectedAsset = await AutoRouter.of(context)
|
||||||
|
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
|
||||||
|
|
||||||
|
if (selectedAsset == null) {
|
||||||
|
ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildTitleInputField() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 10,
|
||||||
|
left: 10,
|
||||||
|
),
|
||||||
|
child: AlbumTitleTextField(
|
||||||
|
isAlbumTitleEmpty: isAlbumTitleEmpty,
|
||||||
|
albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode,
|
||||||
|
albumTitleController: albumTitleController,
|
||||||
|
isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildTitle() {
|
||||||
|
if (selectedAssets.isEmpty) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 200, left: 18),
|
||||||
|
child: const Text(
|
||||||
|
'create_shared_album_page_share_add_assets',
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SliverToBoxAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildSelectPhotosButton() {
|
||||||
|
if (selectedAssets.isEmpty) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Color.fromARGB(255, 206, 206, 206),
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: _onSelectPhotosButtonPressed,
|
||||||
|
icon: const Icon(Icons.add_rounded),
|
||||||
|
label: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Text(
|
||||||
|
'create_shared_album_page_share_select_photos',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SliverToBoxAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildControlButton() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 30,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: [
|
||||||
|
AlbumActionOutlinedButton(
|
||||||
|
iconData: Icons.add_photo_alternate_outlined,
|
||||||
|
onPressed: _onSelectPhotosButtonPressed,
|
||||||
|
labelText: "share_add_photos".tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildSelectedImageGrid() {
|
||||||
|
if (selectedAssets.isNotEmpty) {
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
crossAxisSpacing: 5.0,
|
||||||
|
mainAxisSpacing: 5,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(BuildContext context, int index) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _onBackgroundTapped,
|
||||||
|
child: SharedAlbumThumbnailImage(
|
||||||
|
asset: selectedAssets.toList()[index],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: selectedAssets.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SliverToBoxAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
_createNonSharedAlbum() async {
|
||||||
|
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
||||||
|
ref.watch(albumTitleProvider),
|
||||||
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newAlbum != null) {
|
||||||
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
|
ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||||
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
|
|
||||||
|
AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: false,
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close_rounded),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'share_create_album',
|
||||||
|
style: TextStyle(color: Colors.black),
|
||||||
|
).tr(),
|
||||||
|
actions: [
|
||||||
|
if (isSharedAlbum)
|
||||||
|
TextButton(
|
||||||
|
onPressed: albumTitleController.text.isNotEmpty
|
||||||
|
? _showSelectUserPage
|
||||||
|
: null,
|
||||||
|
child: Text(
|
||||||
|
'create_shared_album_page_share'.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isSharedAlbum)
|
||||||
|
TextButton(
|
||||||
|
onPressed: albumTitleController.text.isNotEmpty &&
|
||||||
|
selectedAssets.isNotEmpty
|
||||||
|
? _createNonSharedAlbum
|
||||||
|
: null,
|
||||||
|
child: Text(
|
||||||
|
'create_shared_album_page_create'.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: GestureDetector(
|
||||||
|
onTap: _onBackgroundTapped,
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
elevation: 5,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
// leading: Container(),
|
||||||
|
pinned: true,
|
||||||
|
floating: false,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(66.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildTitleInputField(),
|
||||||
|
if (selectedAssets.isNotEmpty) _buildControlButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildTitle(),
|
||||||
|
_buildSelectPhotosButton(),
|
||||||
|
_buildSelectedImageGrid(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
116
mobile/lib/modules/album/views/library_page.dart
Normal file
116
mobile/lib/modules/album/views/library_page.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
class LibraryPage extends HookConsumerWidget {
|
||||||
|
const LibraryPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albums = ref.watch(albumProvider);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
ref.read(albumProvider.notifier).getAllAlbums();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildAppBar() {
|
||||||
|
return SliverAppBar(
|
||||||
|
centerTitle: true,
|
||||||
|
floating: true,
|
||||||
|
pinned: false,
|
||||||
|
snap: false,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
title: Text(
|
||||||
|
'IMMICH',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SnowburstOne',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 22,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCreateAlbumButton() {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false));
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: MediaQuery.of(context).size.width / 2 - 18,
|
||||||
|
height: MediaQuery.of(context).size.width / 2 - 18,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
size: 28,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"New album",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
_buildAppBar(),
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(12.0),
|
||||||
|
child: Text(
|
||||||
|
"Albums",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.only(left: 12.0, right: 12, bottom: 50),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
_buildCreateAlbumButton(),
|
||||||
|
for (var album in albums)
|
||||||
|
AlbumThumbnailCard(
|
||||||
|
album: album,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
final SharedAlbum albumInfo;
|
final AlbumResponseDto albumInfo;
|
||||||
|
|
||||||
const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo})
|
const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
AsyncValue<List<User>> suggestedShareUsers =
|
AsyncValue<List<UserResponseDto>> suggestedShareUsers =
|
||||||
ref.watch(suggestedSharedUsersProvider);
|
ref.watch(suggestedSharedUsersProvider);
|
||||||
final sharedUsersList = useState<Set<User>>({});
|
final sharedUsersList = useState<Set<UserResponseDto>>({});
|
||||||
|
|
||||||
_addNewUsersHandler() {
|
_addNewUsersHandler() {
|
||||||
AutoRouter.of(context)
|
AutoRouter.of(context)
|
||||||
.pop(sharedUsersList.value.map((e) => e.id).toList());
|
.pop(sharedUsersList.value.map((e) => e.id).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildTileIcon(User user) {
|
_buildTileIcon(UserResponseDto user) {
|
||||||
if (sharedUsersList.value.contains(user)) {
|
if (sharedUsersList.value.contains(user)) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
@@ -42,7 +42,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildUserList(List<User> users) {
|
_buildUserList(List<UserResponseDto> users) {
|
||||||
List<Widget> usersChip = [];
|
List<Widget> usersChip = [];
|
||||||
|
|
||||||
for (var user in sharedUsersList.value) {
|
for (var user in sharedUsersList.value) {
|
||||||
@@ -54,9 +54,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
label: Text(
|
label: Text(
|
||||||
user.email,
|
user.email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -68,14 +69,15 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
Wrap(
|
Wrap(
|
||||||
children: [...usersChip],
|
children: [...usersChip],
|
||||||
),
|
),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Suggestions',
|
'select_additional_user_for_sharing_page_suggestions'.tr(),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListView.builder(
|
ListView.builder(
|
||||||
@@ -86,13 +88,16 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
title: Text(
|
title: Text(
|
||||||
users[index].email,
|
users[index].email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14, fontWeight: FontWeight.bold),
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (sharedUsersList.value.contains(users[index])) {
|
if (sharedUsersList.value.contains(users[index])) {
|
||||||
sharedUsersList.value = sharedUsersList.value
|
sharedUsersList.value = sharedUsersList.value
|
||||||
.where((selectedUser) =>
|
.where(
|
||||||
selectedUser.id != users[index].id)
|
(selectedUser) => selectedUser.id != users[index].id,
|
||||||
|
)
|
||||||
.toSet();
|
.toSet();
|
||||||
} else {
|
} else {
|
||||||
sharedUsersList.value = {
|
sharedUsersList.value = {
|
||||||
@@ -112,9 +117,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Invite to album',
|
'share_invite',
|
||||||
style: TextStyle(color: Colors.black),
|
style: TextStyle(color: Colors.black),
|
||||||
),
|
).tr(),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@@ -128,9 +133,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
onPressed:
|
onPressed:
|
||||||
sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
|
sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"Add",
|
"share_add",
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -138,7 +143,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
data: (users) {
|
data: (users) {
|
||||||
for (var sharedUsers in albumInfo.sharedUsers) {
|
for (var sharedUsers in albumInfo.sharedUsers) {
|
||||||
users.removeWhere(
|
users.removeWhere(
|
||||||
(u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId);
|
(u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildUserList(users);
|
return _buildUserList(users);
|
||||||
@@ -1,33 +1,34 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SelectUserForSharingPage extends HookConsumerWidget {
|
class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
const SelectUserForSharingPage({Key? key}) : super(key: key);
|
const SelectUserForSharingPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final sharedUsersList = useState<Set<User>>({});
|
final sharedUsersList = useState<Set<UserResponseDto>>({});
|
||||||
AsyncValue<List<User>> suggestedShareUsers =
|
AsyncValue<List<UserResponseDto>> suggestedShareUsers =
|
||||||
ref.watch(suggestedSharedUsersProvider);
|
ref.watch(suggestedSharedUsersProvider);
|
||||||
|
|
||||||
_createSharedAlbum() async {
|
_createSharedAlbum() async {
|
||||||
var isSuccess =
|
var newAlbum =
|
||||||
await ref.watch(sharedAlbumServiceProvider).createSharedAlbum(
|
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
|
||||||
ref.watch(albumTitleProvider),
|
ref.watch(albumTitleProvider),
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
|
||||||
sharedUsersList.value.map((userInfo) => userInfo.id).toList(),
|
sharedUsersList.value.map((userInfo) => userInfo.id).toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (newAlbum != null) {
|
||||||
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
@@ -36,11 +37,14 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScaffoldMessenger(
|
ScaffoldMessenger(
|
||||||
child: SnackBar(content: Text('Failed to create album')));
|
child: SnackBar(
|
||||||
|
content: const Text('select_user_for_sharing_page_err_album').tr(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildTileIcon(User user) {
|
_buildTileIcon(UserResponseDto user) {
|
||||||
if (sharedUsersList.value.contains(user)) {
|
if (sharedUsersList.value.contains(user)) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
@@ -58,7 +62,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildUserList(List<User> users) {
|
_buildUserList(List<UserResponseDto> users) {
|
||||||
List<Widget> usersChip = [];
|
List<Widget> usersChip = [];
|
||||||
|
|
||||||
for (var user in sharedUsersList.value) {
|
for (var user in sharedUsersList.value) {
|
||||||
@@ -70,9 +74,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
label: Text(
|
label: Text(
|
||||||
user.email,
|
user.email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -84,15 +89,16 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
Wrap(
|
Wrap(
|
||||||
children: [...usersChip],
|
children: [...usersChip],
|
||||||
),
|
),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(
|
child: const Text(
|
||||||
'Suggestions',
|
'select_user_for_sharing_page_share_suggestions',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
).tr(),
|
||||||
),
|
),
|
||||||
ListView.builder(
|
ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@@ -102,13 +108,16 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
title: Text(
|
title: Text(
|
||||||
users[index].email,
|
users[index].email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14, fontWeight: FontWeight.bold),
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (sharedUsersList.value.contains(users[index])) {
|
if (sharedUsersList.value.contains(users[index])) {
|
||||||
sharedUsersList.value = sharedUsersList.value
|
sharedUsersList.value = sharedUsersList.value
|
||||||
.where((selectedUser) =>
|
.where(
|
||||||
selectedUser.id != users[index].id)
|
(selectedUser) => selectedUser.id != users[index].id,
|
||||||
|
)
|
||||||
.toSet();
|
.toSet();
|
||||||
} else {
|
} else {
|
||||||
sharedUsersList.value = {
|
sharedUsersList.value = {
|
||||||
@@ -128,9 +137,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Invite to album',
|
'share_invite',
|
||||||
style: TextStyle(color: Colors.black),
|
style: TextStyle(color: Colors.black),
|
||||||
),
|
).tr(),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@@ -141,12 +150,13 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
|
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"Create Album",
|
"share_create_album",
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
))
|
).tr(),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: suggestedShareUsers.when(
|
body: suggestedShareUsers.when(
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/sharing_sliver_appbar.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:transparent_image/transparent_image.dart';
|
import 'package:transparent_image/transparent_image.dart';
|
||||||
|
|
||||||
class SharingPage extends HookConsumerWidget {
|
class SharingPage extends HookConsumerWidget {
|
||||||
@@ -17,13 +18,15 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
||||||
final List<SharedAlbum> sharedAlbums = ref.watch(sharedAlbumProvider);
|
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
() {
|
||||||
|
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
_buildAlbumList() {
|
_buildAlbumList() {
|
||||||
return SliverList(
|
return SliverList(
|
||||||
@@ -59,9 +62,10 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.grey.shade800),
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context)
|
AutoRouter.of(context)
|
||||||
@@ -104,20 +108,20 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'EMPTY LIST',
|
'sharing_page_empty_list',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Create shared albums to share photos and videos with people in your network.',
|
'sharing_page_description',
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -131,15 +135,15 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
const SharingSliverAppBar(),
|
const SharingSliverAppBar(),
|
||||||
const SliverPadding(
|
SliverPadding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
sliver: SliverToBoxAdapter(
|
sliver: SliverToBoxAdapter(
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Shared albums",
|
"sharing_page_album",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
sharedAlbums.isNotEmpty
|
sharedAlbums.isNotEmpty
|
||||||
@@ -3,17 +3,20 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
final ImageViewerService _imageViewerService = ImageViewerService();
|
final ImageViewerService _imageViewerService;
|
||||||
|
|
||||||
ImageViewerStateNotifier()
|
ImageViewerStateNotifier(this._imageViewerService)
|
||||||
: super(ImageViewerPageState(
|
: super(
|
||||||
downloadAssetStatus: DownloadAssetStatus.idle));
|
ImageViewerPageState(
|
||||||
|
downloadAssetStatus: DownloadAssetStatus.idle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
void downloadAsset(ImmichAsset asset, BuildContext context) async {
|
void downloadAsset(AssetResponseDto asset, BuildContext context) async {
|
||||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
||||||
|
|
||||||
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
||||||
@@ -43,4 +46,5 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
|||||||
|
|
||||||
final imageViewerStateProvider =
|
final imageViewerStateProvider =
|
||||||
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
|
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
|
||||||
((ref) => ImageViewerStateNotifier()));
|
((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,33 +1,37 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
final imageViewerServiceProvider =
|
||||||
|
Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider)));
|
||||||
|
|
||||||
class ImageViewerService {
|
class ImageViewerService {
|
||||||
Future<bool> downloadAssetToDevice(ImmichAsset asset) async {
|
final ApiService _apiService;
|
||||||
|
|
||||||
|
ImageViewerService(this._apiService);
|
||||||
|
|
||||||
|
Future<bool> downloadAssetToDevice(AssetResponseDto asset) async {
|
||||||
try {
|
try {
|
||||||
String fileName = p.basename(asset.originalPath);
|
String fileName = p.basename(asset.originalPath);
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
|
||||||
Uri filePath = Uri.parse(
|
|
||||||
"$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
|
|
||||||
|
|
||||||
var res = await http.get(
|
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||||
filePath,
|
asset.deviceAssetId,
|
||||||
headers: {
|
asset.deviceId,
|
||||||
"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"
|
isThumb: false,
|
||||||
},
|
isWeb: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final AssetEntity? entity;
|
||||||
|
|
||||||
if (asset.type == 'IMAGE') {
|
if (asset.type == AssetTypeEnum.IMAGE) {
|
||||||
entity = await PhotoManager.editor.saveImage(
|
entity = await PhotoManager.editor.saveImage(
|
||||||
res.bodyBytes,
|
res.bodyBytes,
|
||||||
title: p.basename(asset.originalPath),
|
title: p.basename(asset.originalPath),
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
class ExifBottomSheet extends ConsumerWidget {
|
class ExifBottomSheet extends ConsumerWidget {
|
||||||
final ImmichAssetWithExif assetDetail;
|
final AssetResponseDto assetDetail;
|
||||||
|
|
||||||
const ExifBottomSheet({Key? key, required this.assetDetail})
|
const ExifBottomSheet({Key? key, required this.assetDetail})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -25,8 +25,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
center: LatLng(assetDetail.exifInfo!.latitude!,
|
center: LatLng(
|
||||||
assetDetail.exifInfo!.longitude!),
|
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
||||||
|
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
zoom: 16.0,
|
zoom: 16.0,
|
||||||
),
|
),
|
||||||
layers: [
|
layers: [
|
||||||
@@ -45,10 +47,13 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
markers: [
|
markers: [
|
||||||
Marker(
|
Marker(
|
||||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
point: LatLng(assetDetail.exifInfo!.latitude!,
|
point: LatLng(
|
||||||
assetDetail.exifInfo!.longitude!),
|
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
||||||
|
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
builder: (ctx) => const Image(
|
builder: (ctx) => const Image(
|
||||||
image: AssetImage('assets/location-pin.png')),
|
image: AssetImage('assets/location-pin.png'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -62,7 +67,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
return Text(
|
return Text(
|
||||||
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12, color: Colors.grey[200], fontWeight: FontWeight.bold),
|
fontSize: 12,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +80,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
||||||
Text(
|
Text(
|
||||||
DateFormat('E, LLL d, y • h:mm a').format(
|
DateFormat('date_format'.tr()).format(
|
||||||
DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
|
assetDetail.exifInfo!.dateTimeOriginal!,
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[400],
|
color: Colors.grey[400],
|
||||||
@@ -84,12 +92,12 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Add Description...",
|
"exif_bottom_sheet_description",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
@@ -104,9 +112,9 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"LOCATION",
|
"exif_bottom_sheet_location",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||||
),
|
).tr(),
|
||||||
if (assetDetail.exifInfo?.latitude != null &&
|
if (assetDetail.exifInfo?.latitude != null &&
|
||||||
assetDetail.exifInfo?.longitude != null)
|
assetDetail.exifInfo?.longitude != null)
|
||||||
_buildMap(),
|
_buildMap(),
|
||||||
@@ -134,9 +142,9 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"DETAILS",
|
"exif_bottom_sheet_details",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
@@ -150,7 +158,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
||||||
? Text(
|
? Text(
|
||||||
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ")
|
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ",
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
if (assetDetail.exifInfo?.make != null)
|
if (assetDetail.exifInfo?.make != null)
|
||||||
@@ -165,7 +174,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
|
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} ",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,21 +15,25 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool allowMoving = _status == _RemoteImageStatus.full;
|
bool allowMoving = _status == _RemoteImageStatus.full;
|
||||||
|
|
||||||
return PhotoView(
|
return PhotoView(
|
||||||
imageProvider: _imageProvider,
|
imageProvider: _imageProvider,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
||||||
enablePanAlways: true,
|
enablePanAlways: true,
|
||||||
scaleStateChangedCallback: _scaleStateChanged,
|
scaleStateChangedCallback: _scaleStateChanged,
|
||||||
onScaleEnd: _onScaleListener);
|
onScaleEnd: _onScaleListener,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScaleListener(BuildContext context, ScaleEndDetails details,
|
void _onScaleListener(
|
||||||
PhotoViewControllerValue controllerValue) {
|
BuildContext context,
|
||||||
|
ScaleEndDetails details,
|
||||||
|
PhotoViewControllerValue controllerValue,
|
||||||
|
) {
|
||||||
// Disable swipe events when zoomed in
|
// Disable swipe events when zoomed in
|
||||||
if (_zoomedIn) return;
|
if (_zoomedIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (controllerValue.position.dy > swipeThreshold) {
|
if (controllerValue.position.dy > swipeThreshold) {
|
||||||
widget.onSwipeDown();
|
widget.onSwipeDown();
|
||||||
} else if (controllerValue.position.dy < -swipeThreshold) {
|
} else if (controllerValue.position.dy < -swipeThreshold) {
|
||||||
@@ -38,16 +42,28 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _scaleStateChanged(PhotoViewScaleState state) {
|
void _scaleStateChanged(PhotoViewScaleState state) {
|
||||||
_zoomedIn = state == PhotoViewScaleState.zoomedIn;
|
// _onScaleListener;
|
||||||
|
_zoomedIn = state != PhotoViewScaleState.initial;
|
||||||
|
if (_zoomedIn) {
|
||||||
|
widget.isZoomedListener.value = true;
|
||||||
|
} else {
|
||||||
|
widget.isZoomedListener.value = false;
|
||||||
|
}
|
||||||
|
widget.isZoomedFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
||||||
return CachedNetworkImageProvider(url,
|
return CachedNetworkImageProvider(
|
||||||
headers: {"Authorization": widget.authToken}, cacheKey: url);
|
url,
|
||||||
|
headers: {"Authorization": widget.authToken},
|
||||||
|
cacheKey: url,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _performStateTransition(
|
void _performStateTransition(
|
||||||
_RemoteImageStatus newStatus, CachedNetworkImageProvider provider) {
|
_RemoteImageStatus newStatus,
|
||||||
|
CachedNetworkImageProvider provider,
|
||||||
|
) {
|
||||||
// Transition to same status is forbidden
|
// Transition to same status is forbidden
|
||||||
if (_status == newStatus) return;
|
if (_status == newStatus) return;
|
||||||
// Transition full -> thumbnail is forbidden
|
// Transition full -> thumbnail is forbidden
|
||||||
@@ -67,19 +83,22 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
_authorizedImageProvider(widget.thumbnailUrl);
|
_authorizedImageProvider(widget.thumbnailUrl);
|
||||||
_imageProvider = thumbnailProvider;
|
_imageProvider = thumbnailProvider;
|
||||||
|
|
||||||
thumbnailProvider
|
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
.resolve(const ImageConfiguration())
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
_performStateTransition(
|
||||||
_performStateTransition(_RemoteImageStatus.thumbnail, thumbnailProvider);
|
_RemoteImageStatus.thumbnail,
|
||||||
}));
|
thumbnailProvider,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
CachedNetworkImageProvider fullProvider =
|
CachedNetworkImageProvider fullProvider =
|
||||||
_authorizedImageProvider(widget.imageUrl);
|
_authorizedImageProvider(widget.imageUrl);
|
||||||
fullProvider
|
fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
.resolve(const ImageConfiguration())
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
||||||
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
}),
|
||||||
}));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -90,14 +109,16 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RemotePhotoView extends StatefulWidget {
|
class RemotePhotoView extends StatefulWidget {
|
||||||
const RemotePhotoView(
|
const RemotePhotoView({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.thumbnailUrl,
|
required this.thumbnailUrl,
|
||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
required this.authToken,
|
required this.authToken,
|
||||||
required this.onSwipeDown,
|
required this.isZoomedFunction,
|
||||||
required this.onSwipeUp})
|
required this.isZoomedListener,
|
||||||
: super(key: key);
|
required this.onSwipeDown,
|
||||||
|
required this.onSwipeUp,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
@@ -105,6 +126,9 @@ class RemotePhotoView extends StatefulWidget {
|
|||||||
|
|
||||||
final void Function() onSwipeDown;
|
final void Function() onSwipeDown;
|
||||||
final void Function() onSwipeUp;
|
final void Function() onSwipeUp;
|
||||||
|
final void Function() isZoomedFunction;
|
||||||
|
|
||||||
|
final ValueNotifier<bool> isZoomedListener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() {
|
State<StatefulWidget> createState() {
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import 'dart:developer';
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
const TopControlAppBar(
|
const TopControlAppBar({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.onMoreInfoPressed,
|
required this.onMoreInfoPressed,
|
||||||
required this.onDownloadPressed})
|
required this.onDownloadPressed,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
final Function onDownloadPressed;
|
final Function onDownloadPressed;
|
||||||
|
|
||||||
@@ -54,12 +54,13 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
: const Icon(Icons.favorite_border_rounded),
|
: const Icon(Icons.favorite_border_rounded),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onMoreInfoPressed();
|
onMoreInfoPressed();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.more_horiz_rounded))
|
icon: const Icon(Icons.more_horiz_rounded),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
134
mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
Normal file
134
mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class GalleryViewerPage extends HookConsumerWidget {
|
||||||
|
late List<AssetResponseDto> assetList;
|
||||||
|
final AssetResponseDto asset;
|
||||||
|
final String thumbnailRequestUrl;
|
||||||
|
|
||||||
|
GalleryViewerPage({
|
||||||
|
Key? key,
|
||||||
|
required this.assetList,
|
||||||
|
required this.asset,
|
||||||
|
required this.thumbnailRequestUrl,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
AssetResponseDto? assetDetail;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final Box<dynamic> box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
|
int indexOfAsset = assetList.indexOf(asset);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState(int index) {
|
||||||
|
indexOfAsset = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
PageController controller =
|
||||||
|
PageController(initialPage: assetList.indexOf(asset));
|
||||||
|
|
||||||
|
getAssetExif() async {
|
||||||
|
assetDetail = await ref
|
||||||
|
.watch(assetServiceProvider)
|
||||||
|
.getAssetById(assetList[indexOfAsset].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showInfo() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
barrierColor: Colors.transparent,
|
||||||
|
isScrollControlled: false,
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return ExifBottomSheet(assetDetail: assetDetail!);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final isZoomed = useState<bool>(false);
|
||||||
|
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
|
||||||
|
|
||||||
|
//make isZoomed listener call instead
|
||||||
|
void isZoomedMethod() {
|
||||||
|
if (isZoomedListener.value) {
|
||||||
|
isZoomed.value = true;
|
||||||
|
} else {
|
||||||
|
isZoomed.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: TopControlAppBar(
|
||||||
|
asset: assetList[indexOfAsset],
|
||||||
|
onMoreInfoPressed: () {
|
||||||
|
showInfo();
|
||||||
|
},
|
||||||
|
onDownloadPressed: () {
|
||||||
|
ref
|
||||||
|
.watch(imageViewerStateProvider.notifier)
|
||||||
|
.downloadAsset(assetList[indexOfAsset], context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: controller,
|
||||||
|
pageSnapping: true,
|
||||||
|
physics: isZoomed.value
|
||||||
|
? const NeverScrollableScrollPhysics()
|
||||||
|
: const BouncingScrollPhysics(),
|
||||||
|
itemCount: assetList.length,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
initState(index);
|
||||||
|
getAssetExif();
|
||||||
|
if (assetList[index].type == AssetTypeEnum.IMAGE) {
|
||||||
|
return ImageViewerPage(
|
||||||
|
thumbnailUrl:
|
||||||
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${assetList[index].id}',
|
||||||
|
imageUrl:
|
||||||
|
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}&isThumb=false',
|
||||||
|
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
||||||
|
isZoomedFunction: isZoomedMethod,
|
||||||
|
isZoomedListener: isZoomedListener,
|
||||||
|
asset: assetList[index],
|
||||||
|
heroTag: assetList[index].id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return SwipeDetector(
|
||||||
|
onSwipeDown: (_) {
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
onSwipeUp: (_) {
|
||||||
|
showInfo();
|
||||||
|
},
|
||||||
|
child: Hero(
|
||||||
|
tag: assetList[index].id,
|
||||||
|
child: VideoViewerPage(
|
||||||
|
asset: assetList[index],
|
||||||
|
videoUrl:
|
||||||
|
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,24 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class ImageViewerPage extends HookConsumerWidget {
|
class ImageViewerPage extends HookConsumerWidget {
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
final String authToken;
|
||||||
ImmichAssetWithExif? assetDetail;
|
final ValueNotifier<bool> isZoomedListener;
|
||||||
|
final void Function() isZoomedFunction;
|
||||||
|
|
||||||
ImageViewerPage({
|
ImageViewerPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -29,19 +26,30 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
required this.heroTag,
|
required this.heroTag,
|
||||||
required this.thumbnailUrl,
|
required this.thumbnailUrl,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
|
required this.authToken,
|
||||||
|
required this.isZoomedFunction,
|
||||||
|
required this.isZoomedListener,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
AssetResponseDto? assetDetail;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final downloadAssetStatus =
|
final downloadAssetStatus =
|
||||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail =
|
assetDetail =
|
||||||
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
getAssetExif();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
showInfo() {
|
showInfo() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
@@ -49,48 +57,32 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
isScrollControlled: false,
|
isScrollControlled: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ExifBottomSheet(assetDetail: assetDetail!);
|
return ExifBottomSheet(assetDetail: assetDetail ?? asset);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
return Stack(
|
||||||
getAssetExif();
|
children: [
|
||||||
return null;
|
Center(
|
||||||
}, []);
|
child: Hero(
|
||||||
|
tag: heroTag,
|
||||||
return Scaffold(
|
child: RemotePhotoView(
|
||||||
backgroundColor: Colors.black,
|
thumbnailUrl: thumbnailUrl,
|
||||||
appBar: TopControlAppBar(
|
imageUrl: imageUrl,
|
||||||
asset: asset,
|
authToken: authToken,
|
||||||
onMoreInfoPressed: showInfo,
|
isZoomedFunction: isZoomedFunction,
|
||||||
onDownloadPressed: () {
|
isZoomedListener: isZoomedListener,
|
||||||
ref
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
.watch(imageViewerStateProvider.notifier)
|
onSwipeUp: () => showInfo(),
|
||||||
.downloadAsset(asset, context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: Hero(
|
|
||||||
tag: heroTag,
|
|
||||||
child: RemotePhotoView(
|
|
||||||
thumbnailUrl: thumbnailUrl,
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
authToken: "Bearer ${box.get(accessTokenKey)}",
|
|
||||||
onSwipeDown: () => AutoRouter.of(context).pop(),
|
|
||||||
onSwipeUp: () => showInfo(),
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
),
|
||||||
const Center(
|
|
||||||
child: DownloadLoadingIndicator(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
|
const Center(
|
||||||
|
child: DownloadLoadingIndicator(),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
@@ -9,18 +6,14 @@ import 'package:chewie/chewie.dart';
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class VideoViewerPage extends HookConsumerWidget {
|
class VideoViewerPage extends HookConsumerWidget {
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
ImmichAssetWithExif? assetDetail;
|
AssetResponseDto? assetDetail;
|
||||||
|
|
||||||
VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
|
VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -32,63 +25,17 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
|
||||||
void showInfo() {
|
return Stack(
|
||||||
showModalBottomSheet(
|
children: [
|
||||||
backgroundColor: Colors.black,
|
VideoThumbnailPlayer(
|
||||||
barrierColor: Colors.transparent,
|
url: videoUrl,
|
||||||
isScrollControlled: false,
|
jwtToken: jwtToken,
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return ExifBottomSheet(assetDetail: assetDetail!);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAssetExif() async {
|
|
||||||
assetDetail =
|
|
||||||
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
getAssetExif();
|
|
||||||
return null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
appBar: TopControlAppBar(
|
|
||||||
asset: asset,
|
|
||||||
onMoreInfoPressed: () {
|
|
||||||
showInfo();
|
|
||||||
},
|
|
||||||
onDownloadPressed: () {
|
|
||||||
ref
|
|
||||||
.watch(imageViewerStateProvider.notifier)
|
|
||||||
.downloadAsset(asset, context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
body: SwipeDetector(
|
|
||||||
onSwipeDown: (_) {
|
|
||||||
AutoRouter.of(context).pop();
|
|
||||||
},
|
|
||||||
onSwipeUp: (_) {
|
|
||||||
showInfo();
|
|
||||||
},
|
|
||||||
child: SafeArea(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
VideoThumbnailPlayer(
|
|
||||||
url: videoUrl,
|
|
||||||
jwtToken: jwtToken,
|
|
||||||
),
|
|
||||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
|
||||||
const Center(
|
|
||||||
child: DownloadLoadingIndicator(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
|
const Center(
|
||||||
|
child: DownloadLoadingIndicator(),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,8 +63,10 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
|||||||
|
|
||||||
Future<void> initializePlayer() async {
|
Future<void> initializePlayer() async {
|
||||||
try {
|
try {
|
||||||
videoPlayerController = VideoPlayerController.network(widget.url,
|
videoPlayerController = VideoPlayerController.network(
|
||||||
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"});
|
widget.url,
|
||||||
|
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
||||||
|
);
|
||||||
|
|
||||||
await videoPlayerController.initialize();
|
await videoPlayerController.initialize();
|
||||||
_createChewieController();
|
_createChewieController();
|
||||||
@@ -130,10 +79,13 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
|||||||
_createChewieController() {
|
_createChewieController() {
|
||||||
chewieController = ChewieController(
|
chewieController = ChewieController(
|
||||||
showOptions: true,
|
showOptions: true,
|
||||||
showControlsOnInitialize: false,
|
showControlsOnInitialize: true,
|
||||||
videoPlayerController: videoPlayerController,
|
videoPlayerController: videoPlayerController,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
autoInitialize: false,
|
autoInitialize: true,
|
||||||
|
allowFullScreen: true,
|
||||||
|
showControls: true,
|
||||||
|
hideControlsTimer: const Duration(seconds: 5),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,11 +105,13 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
|||||||
controller: chewieController!,
|
controller: chewieController!,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox(
|
: const Center(
|
||||||
width: 75,
|
child: SizedBox(
|
||||||
height: 75,
|
width: 75,
|
||||||
child: CircularProgressIndicator.adaptive(
|
height: 75,
|
||||||
strokeWidth: 2,
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
|
|
||||||
enum BackUpProgressEnum { idle, inProgress, done }
|
enum BackUpProgressEnum { idle, inProgress, done }
|
||||||
|
|
||||||
class BackUpState extends Equatable {
|
class BackUpState {
|
||||||
// enum
|
// enum
|
||||||
final BackUpProgressEnum backupProgress;
|
final BackUpProgressEnum backupProgress;
|
||||||
final List<String> allAssetsInDatabase;
|
final List<String> allAssetsInDatabase;
|
||||||
final double progressInPercentage;
|
final double progressInPercentage;
|
||||||
final CancellationToken cancelToken;
|
final CancellationToken cancelToken;
|
||||||
final ServerInfo serverInfo;
|
final ServerInfoResponseDto serverInfo;
|
||||||
|
|
||||||
/// All available albums on the device
|
/// All available albums on the device
|
||||||
final List<AvailableAlbum> availableAlbums;
|
final List<AvailableAlbum> availableAlbums;
|
||||||
@@ -26,6 +27,9 @@ class BackUpState extends Equatable {
|
|||||||
/// All assets from the selected albums that have been backup
|
/// All assets from the selected albums that have been backup
|
||||||
final Set<String> selectedAlbumsBackupAssetsIds;
|
final Set<String> selectedAlbumsBackupAssetsIds;
|
||||||
|
|
||||||
|
// Current Backup Asset
|
||||||
|
final CurrentUploadAsset currentUploadAsset;
|
||||||
|
|
||||||
const BackUpState({
|
const BackUpState({
|
||||||
required this.backupProgress,
|
required this.backupProgress,
|
||||||
required this.allAssetsInDatabase,
|
required this.allAssetsInDatabase,
|
||||||
@@ -37,6 +41,7 @@ class BackUpState extends Equatable {
|
|||||||
required this.excludedBackupAlbums,
|
required this.excludedBackupAlbums,
|
||||||
required this.allUniqueAssets,
|
required this.allUniqueAssets,
|
||||||
required this.selectedAlbumsBackupAssetsIds,
|
required this.selectedAlbumsBackupAssetsIds,
|
||||||
|
required this.currentUploadAsset,
|
||||||
});
|
});
|
||||||
|
|
||||||
BackUpState copyWith({
|
BackUpState copyWith({
|
||||||
@@ -44,12 +49,13 @@ class BackUpState extends Equatable {
|
|||||||
List<String>? allAssetsInDatabase,
|
List<String>? allAssetsInDatabase,
|
||||||
double? progressInPercentage,
|
double? progressInPercentage,
|
||||||
CancellationToken? cancelToken,
|
CancellationToken? cancelToken,
|
||||||
ServerInfo? serverInfo,
|
ServerInfoResponseDto? serverInfo,
|
||||||
List<AvailableAlbum>? availableAlbums,
|
List<AvailableAlbum>? availableAlbums,
|
||||||
Set<AssetPathEntity>? selectedBackupAlbums,
|
Set<AssetPathEntity>? selectedBackupAlbums,
|
||||||
Set<AssetPathEntity>? excludedBackupAlbums,
|
Set<AssetPathEntity>? excludedBackupAlbums,
|
||||||
Set<AssetEntity>? allUniqueAssets,
|
Set<AssetEntity>? allUniqueAssets,
|
||||||
Set<String>? selectedAlbumsBackupAssetsIds,
|
Set<String>? selectedAlbumsBackupAssetsIds,
|
||||||
|
CurrentUploadAsset? currentUploadAsset,
|
||||||
}) {
|
}) {
|
||||||
return BackUpState(
|
return BackUpState(
|
||||||
backupProgress: backupProgress ?? this.backupProgress,
|
backupProgress: backupProgress ?? this.backupProgress,
|
||||||
@@ -63,27 +69,49 @@ class BackUpState extends Equatable {
|
|||||||
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
|
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
|
||||||
selectedAlbumsBackupAssetsIds:
|
selectedAlbumsBackupAssetsIds:
|
||||||
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
|
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
|
||||||
|
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
|
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props {
|
bool operator ==(Object other) {
|
||||||
return [
|
if (identical(this, other)) return true;
|
||||||
backupProgress,
|
final collectionEquals = const DeepCollectionEquality().equals;
|
||||||
allAssetsInDatabase,
|
|
||||||
progressInPercentage,
|
return other is BackUpState &&
|
||||||
cancelToken,
|
other.backupProgress == backupProgress &&
|
||||||
serverInfo,
|
collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) &&
|
||||||
availableAlbums,
|
other.progressInPercentage == progressInPercentage &&
|
||||||
selectedBackupAlbums,
|
other.cancelToken == cancelToken &&
|
||||||
excludedBackupAlbums,
|
other.serverInfo == serverInfo &&
|
||||||
allUniqueAssets,
|
collectionEquals(other.availableAlbums, availableAlbums) &&
|
||||||
selectedAlbumsBackupAssetsIds,
|
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
|
||||||
];
|
collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
|
||||||
|
collectionEquals(other.allUniqueAssets, allUniqueAssets) &&
|
||||||
|
collectionEquals(
|
||||||
|
other.selectedAlbumsBackupAssetsIds,
|
||||||
|
selectedAlbumsBackupAssetsIds,
|
||||||
|
) &&
|
||||||
|
other.currentUploadAsset == currentUploadAsset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return backupProgress.hashCode ^
|
||||||
|
allAssetsInDatabase.hashCode ^
|
||||||
|
progressInPercentage.hashCode ^
|
||||||
|
cancelToken.hashCode ^
|
||||||
|
serverInfo.hashCode ^
|
||||||
|
availableAlbums.hashCode ^
|
||||||
|
selectedBackupAlbums.hashCode ^
|
||||||
|
excludedBackupAlbums.hashCode ^
|
||||||
|
allUniqueAssets.hashCode ^
|
||||||
|
selectedAlbumsBackupAssetsIds.hashCode ^
|
||||||
|
currentUploadAsset.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class CurrentUploadAsset {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final String fileName;
|
||||||
|
final String fileType;
|
||||||
|
|
||||||
|
CurrentUploadAsset({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileType,
|
||||||
|
});
|
||||||
|
|
||||||
|
CurrentUploadAsset copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
String? fileName,
|
||||||
|
String? fileType,
|
||||||
|
}) {
|
||||||
|
return CurrentUploadAsset(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
fileName: fileName ?? this.fileName,
|
||||||
|
fileType: fileType ?? this.fileType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'id': id});
|
||||||
|
result.addAll({'createdAt': createdAt.millisecondsSinceEpoch});
|
||||||
|
result.addAll({'fileName': fileName});
|
||||||
|
result.addAll({'fileType': fileType});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CurrentUploadAsset.fromMap(Map<String, dynamic> map) {
|
||||||
|
return CurrentUploadAsset(
|
||||||
|
id: map['id'] ?? '',
|
||||||
|
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']),
|
||||||
|
fileName: map['fileName'] ?? '',
|
||||||
|
fileType: map['fileType'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory CurrentUploadAsset.fromJson(String source) =>
|
||||||
|
CurrentUploadAsset.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CurrentUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is CurrentUploadAsset &&
|
||||||
|
other.id == id &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.fileName == fileName &&
|
||||||
|
other.fileType == fileType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
fileName.hashCode ^
|
||||||
|
fileType.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class ErrorUploadAsset extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final String fileName;
|
||||||
|
final String fileType;
|
||||||
|
final AssetEntity asset;
|
||||||
|
final String errorMessage;
|
||||||
|
|
||||||
|
const ErrorUploadAsset({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileType,
|
||||||
|
required this.asset,
|
||||||
|
required this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
ErrorUploadAsset copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
String? fileName,
|
||||||
|
String? fileType,
|
||||||
|
AssetEntity? asset,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return ErrorUploadAsset(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
fileName: fileName ?? this.fileName,
|
||||||
|
fileType: fileType ?? this.fileType,
|
||||||
|
asset: asset ?? this.asset,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ErrorUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props {
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
fileName,
|
||||||
|
fileType,
|
||||||
|
errorMessage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,28 +5,35 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
BackupNotifier(this._backupService, this._serverInfoService, this._authState)
|
BackupNotifier(
|
||||||
: super(
|
this._backupService,
|
||||||
|
this._serverInfoService,
|
||||||
|
this._authState,
|
||||||
|
this.ref,
|
||||||
|
) : super(
|
||||||
BackUpState(
|
BackUpState(
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
allAssetsInDatabase: const [],
|
allAssetsInDatabase: const [],
|
||||||
progressInPercentage: 0,
|
progressInPercentage: 0,
|
||||||
cancelToken: CancellationToken(),
|
cancelToken: CancellationToken(),
|
||||||
serverInfo: ServerInfo(
|
serverInfo: ServerInfoResponseDto(
|
||||||
diskAvailable: "0",
|
diskAvailable: "0",
|
||||||
diskAvailableRaw: 0,
|
diskAvailableRaw: 0,
|
||||||
diskSize: "0",
|
diskSize: "0",
|
||||||
diskSizeRaw: 0,
|
diskSizeRaw: 0,
|
||||||
diskUsagePercentage: 0.0,
|
diskUsagePercentage: 0,
|
||||||
diskUse: "0",
|
diskUse: "0",
|
||||||
diskUseRaw: 0,
|
diskUseRaw: 0,
|
||||||
),
|
),
|
||||||
@@ -35,6 +42,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
excludedBackupAlbums: const {},
|
excludedBackupAlbums: const {},
|
||||||
allUniqueAssets: const {},
|
allUniqueAssets: const {},
|
||||||
selectedAlbumsBackupAssetsIds: const {},
|
selectedAlbumsBackupAssetsIds: const {},
|
||||||
|
currentUploadAsset: CurrentUploadAsset(
|
||||||
|
id: '...',
|
||||||
|
createdAt: DateTime.parse('2020-10-04'),
|
||||||
|
fileName: '...',
|
||||||
|
fileType: '...',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
getBackupInfo();
|
getBackupInfo();
|
||||||
@@ -43,6 +56,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final BackupService _backupService;
|
final BackupService _backupService;
|
||||||
final ServerInfoService _serverInfoService;
|
final ServerInfoService _serverInfoService;
|
||||||
final AuthenticationState _authState;
|
final AuthenticationState _authState;
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// UI INTERACTION
|
/// UI INTERACTION
|
||||||
@@ -99,7 +113,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
// Get all albums on the device
|
// Get all albums on the device
|
||||||
List<AvailableAlbum> availableAlbums = [];
|
List<AvailableAlbum> availableAlbums = [];
|
||||||
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
||||||
hasAll: true, type: RequestType.common);
|
hasAll: true,
|
||||||
|
type: RequestType.common,
|
||||||
|
);
|
||||||
|
|
||||||
for (AssetPathEntity album in albums) {
|
for (AssetPathEntity album in albums) {
|
||||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||||
@@ -142,7 +158,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Get album that contains all assets
|
// Get album that contains all assets
|
||||||
var list = await PhotoManager.getAssetPathList(
|
var list = await PhotoManager.getAssetPathList(
|
||||||
hasAll: true, onlyAll: true, type: RequestType.common);
|
hasAll: true,
|
||||||
|
onlyAll: true,
|
||||||
|
type: RequestType.common,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (list.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
AssetPathEntity albumHasAllAssets = list.first;
|
AssetPathEntity albumHasAllAssets = list.first;
|
||||||
|
|
||||||
backupAlbumInfoBox.put(
|
backupAlbumInfoBox.put(
|
||||||
@@ -161,13 +184,15 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
|
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
|
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] Failed to generate album from id $e");
|
debugPrint("[ERROR] Failed to generate album from id $e");
|
||||||
@@ -197,8 +222,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
Set<AssetEntity> allUniqueAssets =
|
Set<AssetEntity> allUniqueAssets =
|
||||||
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
||||||
List<String> allAssetsInDatabase =
|
var allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
|
||||||
await _backupService.getDeviceBackupAsset();
|
|
||||||
|
if (allAssetsInDatabase == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Find asset that were backup from selected albums
|
// Find asset that were backup from selected albums
|
||||||
Set<String> selectedAlbumsBackupAssets =
|
Set<String> selectedAlbumsBackupAssets =
|
||||||
@@ -235,8 +263,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// and then update the UI according to those information
|
/// and then update the UI according to those information
|
||||||
///
|
///
|
||||||
Future<void> getBackupInfo() async {
|
Future<void> getBackupInfo() async {
|
||||||
await _getBackupAlbumsInfo();
|
await Future.wait([
|
||||||
await _updateServerInfo();
|
_getBackupAlbumsInfo(),
|
||||||
|
_updateServerInfo(),
|
||||||
|
]);
|
||||||
|
|
||||||
await _updateBackupAssetCount();
|
await _updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,33 +318,51 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Perform Backup
|
// Perform Backup
|
||||||
state = state.copyWith(cancelToken: CancellationToken());
|
state = state.copyWith(cancelToken: CancellationToken());
|
||||||
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken,
|
_backupService.backupAsset(
|
||||||
_onAssetUploaded, _onUploadProgress);
|
assetsWillBeBackup,
|
||||||
|
state.cancelToken,
|
||||||
|
_onAssetUploaded,
|
||||||
|
_onUploadProgress,
|
||||||
|
_onSetCurrentBackupAsset,
|
||||||
|
_onBackupError,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
PhotoManager.openSetting();
|
PhotoManager.openSetting();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||||
|
ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
||||||
|
state = state.copyWith(currentUploadAsset: currentUploadAsset);
|
||||||
|
}
|
||||||
|
|
||||||
void cancelBackup() {
|
void cancelBackup() {
|
||||||
state.cancelToken.cancel();
|
state.cancelToken.cancel();
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
|
progressInPercentage: 0.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||||
state = state.copyWith(selectedAlbumsBackupAssetsIds: {
|
state = state.copyWith(
|
||||||
...state.selectedAlbumsBackupAssetsIds,
|
selectedAlbumsBackupAssetsIds: {
|
||||||
deviceAssetId
|
...state.selectedAlbumsBackupAssetsIds,
|
||||||
}, allAssetsInDatabase: [
|
deviceAssetId
|
||||||
...state.allAssetsInDatabase,
|
},
|
||||||
deviceAssetId
|
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
|
||||||
]);
|
);
|
||||||
|
|
||||||
if (state.allUniqueAssets.length -
|
if (state.allUniqueAssets.length -
|
||||||
state.selectedAlbumsBackupAssetsIds.length ==
|
state.selectedAlbumsBackupAssetsIds.length ==
|
||||||
0) {
|
0) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
|
backupProgress: BackUpProgressEnum.done,
|
||||||
|
progressInPercentage: 0.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateServerInfo();
|
_updateServerInfo();
|
||||||
@@ -321,24 +370,19 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
void _onUploadProgress(int sent, int total) {
|
void _onUploadProgress(int sent, int total) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
|
progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateServerInfo() async {
|
Future<void> _updateServerInfo() async {
|
||||||
var serverInfo = await _serverInfoService.getServerInfo();
|
var serverInfo = await _serverInfoService.getServerInfo();
|
||||||
|
|
||||||
// Update server info
|
// Update server info
|
||||||
state = state.copyWith(
|
if (serverInfo != null) {
|
||||||
serverInfo: ServerInfo(
|
state = state.copyWith(
|
||||||
diskSize: serverInfo.diskSize,
|
serverInfo: serverInfo,
|
||||||
diskUse: serverInfo.diskUse,
|
);
|
||||||
diskAvailable: serverInfo.diskAvailable,
|
}
|
||||||
diskSizeRaw: serverInfo.diskSizeRaw,
|
|
||||||
diskUseRaw: serverInfo.diskUseRaw,
|
|
||||||
diskAvailableRaw: serverInfo.diskAvailableRaw,
|
|
||||||
diskUsagePercentage: serverInfo.diskUsagePercentage,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void resumeBackup() {
|
void resumeBackup() {
|
||||||
@@ -375,5 +419,6 @@ final backupProvider =
|
|||||||
ref.watch(backupServiceProvider),
|
ref.watch(backupServiceProvider),
|
||||||
ref.watch(serverInfoServiceProvider),
|
ref.watch(serverInfoServiceProvider),
|
||||||
ref.watch(authenticationProvider),
|
ref.watch(authenticationProvider),
|
||||||
|
ref,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
|
|
||||||
|
class ErrorBackupListNotifier extends StateNotifier<Set<ErrorUploadAsset>> {
|
||||||
|
ErrorBackupListNotifier() : super({});
|
||||||
|
|
||||||
|
add(ErrorUploadAsset errorAsset) {
|
||||||
|
state = state.union({errorAsset});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(ErrorUploadAsset errorAsset) {
|
||||||
|
state = state.difference({errorAsset});
|
||||||
|
}
|
||||||
|
|
||||||
|
empty() {
|
||||||
|
state = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final errorBackupListProvider =
|
||||||
|
StateNotifierProvider<ErrorBackupListNotifier, Set<ErrorUploadAsset>>(
|
||||||
|
(ref) => ErrorBackupListNotifier(),
|
||||||
|
);
|
||||||
@@ -2,47 +2,55 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/files_helper.dart';
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:cancellation_token_http/http.dart' as http;
|
import 'package:cancellation_token_http/http.dart' as http;
|
||||||
|
|
||||||
final backupServiceProvider =
|
final backupServiceProvider = Provider(
|
||||||
Provider((ref) => BackupService(ref.watch(networkServiceProvider)));
|
(ref) => BackupService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class BackupService {
|
class BackupService {
|
||||||
final NetworkService _networkService;
|
final ApiService _apiService;
|
||||||
BackupService(this._networkService);
|
|
||||||
|
|
||||||
Future<List<String>> getDeviceBackupAsset() async {
|
BackupService(this._apiService);
|
||||||
|
|
||||||
|
Future<List<String>?> getDeviceBackupAsset() async {
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
|
||||||
Response response =
|
try {
|
||||||
await _networkService.getRequest(url: "asset/$deviceId");
|
return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId);
|
||||||
List<dynamic> result = jsonDecode(response.toString());
|
} catch (e) {
|
||||||
|
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}');
|
||||||
return result.cast<String>();
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backupAsset(
|
backupAsset(
|
||||||
Set<AssetEntity> assetList,
|
Set<AssetEntity> assetList,
|
||||||
http.CancellationToken cancelToken,
|
http.CancellationToken cancelToken,
|
||||||
Function(String, String) singleAssetDoneCb,
|
Function(String, String) singleAssetDoneCb,
|
||||||
Function(int, int) uploadProgress) async {
|
Function(int, int) uploadProgressCb,
|
||||||
|
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
|
||||||
|
Function(ErrorUploadAsset) errorCb,
|
||||||
|
) async {
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
File? file;
|
File? file;
|
||||||
|
|
||||||
http.MultipartFile? thumbnailUploadData;
|
|
||||||
|
|
||||||
for (var entity in assetList) {
|
for (var entity in assetList) {
|
||||||
try {
|
try {
|
||||||
if (entity.type == AssetType.video) {
|
if (entity.type == AssetType.video) {
|
||||||
@@ -72,9 +80,11 @@ class BackupService {
|
|||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
var req = MultipartRequest(
|
var req = MultipartRequest(
|
||||||
'POST', Uri.parse('$savedEndpoint/asset/upload'),
|
'POST',
|
||||||
onProgress: ((bytes, totalBytes) =>
|
Uri.parse('$savedEndpoint/asset/upload'),
|
||||||
uploadProgress(bytes, totalBytes)));
|
onProgress: ((bytes, totalBytes) =>
|
||||||
|
uploadProgressCb(bytes, totalBytes)),
|
||||||
|
);
|
||||||
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
|
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
|
||||||
|
|
||||||
req.fields['deviceAssetId'] = entity.id;
|
req.fields['deviceAssetId'] = entity.id;
|
||||||
@@ -88,10 +98,38 @@ class BackupService {
|
|||||||
|
|
||||||
req.files.add(assetRawUploadData);
|
req.files.add(assetRawUploadData);
|
||||||
|
|
||||||
var res = await req.send(cancellationToken: cancelToken);
|
setCurrentUploadAssetCb(
|
||||||
|
CurrentUploadAsset(
|
||||||
|
id: entity.id,
|
||||||
|
createdAt: entity.createDateTime,
|
||||||
|
fileName: originalFileName,
|
||||||
|
fileType: _getAssetType(entity.type),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (res.statusCode == 201) {
|
var response = await req.send(cancellationToken: cancelToken);
|
||||||
|
|
||||||
|
if (response.statusCode == 201) {
|
||||||
singleAssetDoneCb(entity.id, deviceId);
|
singleAssetDoneCb(entity.id, deviceId);
|
||||||
|
} else {
|
||||||
|
var data = await response.stream.bytesToString();
|
||||||
|
var error = jsonDecode(data);
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
"Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}",
|
||||||
|
);
|
||||||
|
|
||||||
|
errorCb(
|
||||||
|
ErrorUploadAsset(
|
||||||
|
asset: entity,
|
||||||
|
id: entity.id,
|
||||||
|
createdAt: entity.createDateTime,
|
||||||
|
fileName: originalFileName,
|
||||||
|
fileType: _getAssetType(entity.type),
|
||||||
|
errorMessage: error['error'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on http.CancelledException {
|
} on http.CancelledException {
|
||||||
@@ -121,15 +159,29 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DeviceInfoRemote> setAutoBackup(
|
Future<DeviceInfoResponseDto> setAutoBackup(
|
||||||
bool status, String deviceId, String deviceType) async {
|
bool status,
|
||||||
var res = await _networkService.patchRequest(url: 'device-info', data: {
|
String deviceId,
|
||||||
"isAutoBackup": status,
|
DeviceTypeEnum deviceType,
|
||||||
"deviceId": deviceId,
|
) async {
|
||||||
"deviceType": deviceType,
|
try {
|
||||||
});
|
var updatedDeviceInfo = await _apiService.deviceInfoApi.updateDeviceInfo(
|
||||||
|
UpdateDeviceInfoDto(
|
||||||
|
deviceId: deviceId,
|
||||||
|
deviceType: deviceType,
|
||||||
|
isAutoBackup: status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return DeviceInfoRemote.fromJson(res.toString());
|
if (updatedDeviceInfo == null) {
|
||||||
|
throw Exception("Error updating device info");
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedDeviceInfo;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error setAutoBackup: ${e.toString()}");
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
@@ -25,7 +26,9 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||||
|
|
||||||
ColorFilter selectedFilter = ColorFilter.mode(
|
ColorFilter selectedFilter = ColorFilter.mode(
|
||||||
Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken);
|
Theme.of(context).primaryColor.withAlpha(100),
|
||||||
|
BlendMode.darken,
|
||||||
|
);
|
||||||
ColorFilter excludedFilter =
|
ColorFilter excludedFilter =
|
||||||
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
||||||
ColorFilter unselectedFilter =
|
ColorFilter unselectedFilter =
|
||||||
@@ -37,10 +40,13 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"INCLUDED",
|
"album_info_card_backup_album_included",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
fontSize: 10,
|
||||||
),
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
);
|
);
|
||||||
} else if (isExcluded) {
|
} else if (isExcluded) {
|
||||||
@@ -48,10 +54,13 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"EXCLUDED",
|
"album_info_card_backup_album_excluded",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
fontSize: 10,
|
||||||
),
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
backgroundColor: Colors.red[300],
|
backgroundColor: Colors.red[300],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -77,7 +86,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Cannot remove the only album",
|
msg: "backup_err_only_album".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -93,10 +102,12 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
|
|
||||||
if (isExcluded) {
|
if (isExcluded) {
|
||||||
|
// Remove from exclude album list
|
||||||
ref
|
ref
|
||||||
.watch(backupProvider.notifier)
|
.watch(backupProvider.notifier)
|
||||||
.removeExcludedAlbumForBackup(albumInfo);
|
.removeExcludedAlbumForBackup(albumInfo);
|
||||||
} else {
|
} else {
|
||||||
|
// Add to exclude album list
|
||||||
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
|
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
|
||||||
ref
|
ref
|
||||||
.watch(backupProvider)
|
.watch(backupProvider)
|
||||||
@@ -104,7 +115,17 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
.contains(albumInfo)) {
|
.contains(albumInfo)) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Cannot exclude the only album",
|
msg: "backup_err_only_album".tr(),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albumInfo.id == 'isAll') {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'Cannot exclude album contains all assets',
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -137,15 +158,16 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
height: 200,
|
height: 200,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(12),
|
topLeft: Radius.circular(12),
|
||||||
topRight: Radius.circular(12)),
|
topRight: Radius.circular(12),
|
||||||
|
),
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
colorFilter: _buildImageFilter(),
|
colorFilter: _buildImageFilter(),
|
||||||
image: imageData != null
|
image: imageData != null
|
||||||
? MemoryImage(imageData!)
|
? MemoryImage(imageData!)
|
||||||
: const AssetImage(
|
: const AssetImage(
|
||||||
'assets/immich-logo-no-outline.png')
|
'assets/immich-logo-no-outline.png',
|
||||||
as ImageProvider,
|
) as ImageProvider,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -173,16 +195,22 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
Text(
|
Text(
|
||||||
albumInfo.name,
|
albumInfo.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2.0),
|
padding: const EdgeInsets.only(top: 2.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${albumInfo.assetCount} ${(albumInfo.isAll ? " (ALL)" : "")}',
|
albumInfo.assetCount.toString() +
|
||||||
|
(albumInfo.isAll
|
||||||
|
? " (${'backup_all'.tr()})"
|
||||||
|
: ""),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12, color: Colors.grey[600]),
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class BackupInfoCard extends StatelessWidget {
|
class BackupInfoCard extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final String info;
|
final String info;
|
||||||
const BackupInfoCard(
|
const BackupInfoCard({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.subtitle,
|
required this.subtitle,
|
||||||
required this.info})
|
required this.info,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -44,7 +45,7 @@ class BackupInfoCard extends StatelessWidget {
|
|||||||
info,
|
info,
|
||||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const Text("assets"),
|
const Text("backup_info_card_assets").tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
await album.getAssetListRange(start: 0, end: album.assetCount);
|
await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
_getAssetsInAlbum();
|
() {
|
||||||
return null;
|
_getAssetsInAlbum();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -39,9 +42,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
"ID ${album.id}",
|
"ID ${album.id}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -59,9 +63,11 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
itemCount: assets.value.length,
|
itemCount: assets.value.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
Future<Uint8List?> thumbData = assets.value[index]
|
Future<Uint8List?> thumbData =
|
||||||
.thumbnailDataWithSize(const ThumbnailSize(200, 200),
|
assets.value[index].thumbnailDataWithSize(
|
||||||
quality: 50);
|
const ThumbnailSize(200, 200),
|
||||||
|
quality: 50,
|
||||||
|
);
|
||||||
|
|
||||||
return FutureBuilder<Uint8List?>(
|
return FutureBuilder<Uint8List?>(
|
||||||
future: thumbData,
|
future: thumbData,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
@@ -16,10 +17,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
ref.read(backupProvider.notifier).getBackupInfo();
|
() {
|
||||||
return null;
|
ref.read(backupProvider.notifier).getBackupInfo();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
_buildAlbumSelectionList() {
|
_buildAlbumSelectionList() {
|
||||||
if (availableAlbums.isEmpty) {
|
if (availableAlbums.isEmpty) {
|
||||||
@@ -41,8 +45,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
? const EdgeInsets.only(left: 16.00)
|
? const EdgeInsets.only(left: 16.00)
|
||||||
: const EdgeInsets.all(0),
|
: const EdgeInsets.all(0),
|
||||||
child: AlbumInfoCard(
|
child: AlbumInfoCard(
|
||||||
imageData: thumbnailData,
|
imageData: thumbnailData,
|
||||||
albumInfo: availableAlbums[index].albumEntity),
|
albumInfo: availableAlbums[index].albumEntity,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -55,7 +60,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Cannot remove the only album",
|
msg: "backup_err_only_album".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -72,13 +77,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
child: Chip(
|
child: Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5)),
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
deleteIconColor: Colors.white,
|
deleteIconColor: Colors.white,
|
||||||
@@ -108,13 +115,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
child: Chip(
|
child: Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5)),
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.red[300],
|
backgroundColor: Colors.red[300],
|
||||||
deleteIconColor: Colors.white,
|
deleteIconColor: Colors.white,
|
||||||
@@ -136,20 +145,21 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Select Albums",
|
"backup_album_selection_page_select_albums",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
physics: const ClampingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
padding:
|
||||||
child: Text(
|
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||||
"Selection Info",
|
child: const Text(
|
||||||
|
"backup_album_selection_page_selection_info",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
// Selected Album Chips
|
// Selected Album Chips
|
||||||
|
|
||||||
@@ -181,14 +191,19 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
ListTile(
|
ListTile(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
title: Text(
|
title: Text(
|
||||||
"Total unique assets",
|
"backup_album_selection_page_total_assets",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[700]),
|
color: Colors.grey[700],
|
||||||
),
|
),
|
||||||
|
).tr(),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
'${ref.watch(backupProvider).allUniqueAssets.length}',
|
ref
|
||||||
|
.watch(backupProvider)
|
||||||
|
.allUniqueAssets
|
||||||
|
.length
|
||||||
|
.toString(),
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -199,19 +214,20 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"Albums on device (${availableAlbums.length})",
|
"backup_album_selection_page_albums_device"
|
||||||
|
.tr(args: [availableAlbums.length.toString()]),
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Tap to include, double tap to exclude",
|
"backup_album_selection_page_albums_tap",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
splashRadius: 16,
|
splashRadius: 16,
|
||||||
@@ -227,24 +243,27 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
title: Text(
|
title: Text(
|
||||||
'Selection Info',
|
'backup_album_selection_page_selection_info',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: ListBody(
|
child: ListBody(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.',
|
'backup_album_selection_page_assets_scatter',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14, color: Colors.grey[700]),
|
fontSize: 14,
|
||||||
),
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
@@ -24,16 +26,19 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
? false
|
? false
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
() {
|
||||||
ref.watch(backupProvider.notifier).getBackupInfo();
|
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
||||||
}
|
ref.watch(backupProvider.notifier).getBackupInfo();
|
||||||
|
}
|
||||||
|
|
||||||
ref
|
ref
|
||||||
.watch(websocketProvider.notifier)
|
.watch(websocketProvider.notifier)
|
||||||
.stopListenToEvent('on_upload_success');
|
.stopListenToEvent('on_upload_success');
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildStorageInformation() {
|
Widget _buildStorageInformation() {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -42,9 +47,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Server Storage",
|
"backup_controller_page_server_storage",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
).tr(),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -56,7 +61,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
barRadius: const Radius.circular(2),
|
barRadius: const Radius.circular(2),
|
||||||
lineHeight: 6.0,
|
lineHeight: 10.0,
|
||||||
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
||||||
backgroundColor: Colors.grey,
|
backgroundColor: Colors.grey,
|
||||||
progressColor: Theme.of(context).primaryColor,
|
progressColor: Theme.of(context).primaryColor,
|
||||||
@@ -64,8 +69,12 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 12.0),
|
padding: const EdgeInsets.only(top: 12.0),
|
||||||
child: Text(
|
child: const Text('backup_controller_page_storage_format').tr(
|
||||||
'${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'),
|
args: [
|
||||||
|
backupState.serverInfo.diskUse,
|
||||||
|
backupState.serverInfo.diskSize
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -74,11 +83,13 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ListTile _buildBackupController() {
|
ListTile _buildBackupController() {
|
||||||
var backUpOption =
|
var backUpOption = authenticationState.deviceInfo.isAutoBackup
|
||||||
authenticationState.deviceInfo.isAutoBackup ? "on" : "off";
|
? "backup_controller_page_status_on".tr()
|
||||||
|
: "backup_controller_page_status_off".tr();
|
||||||
var isAutoBackup = authenticationState.deviceInfo.isAutoBackup;
|
var isAutoBackup = authenticationState.deviceInfo.isAutoBackup;
|
||||||
var backupBtnText =
|
var backupBtnText = authenticationState.deviceInfo.isAutoBackup
|
||||||
authenticationState.deviceInfo.isAutoBackup ? "off" : "on";
|
? "backup_controller_page_turn_off".tr()
|
||||||
|
: "backup_controller_page_turn_on".tr();
|
||||||
return ListTile(
|
return ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
leading: isAutoBackup
|
leading: isAutoBackup
|
||||||
@@ -88,7 +99,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: const Icon(Icons.cloud_off_rounded),
|
: const Icon(Icons.cloud_off_rounded),
|
||||||
title: Text(
|
title: Text(
|
||||||
"Back up is $backUpOption",
|
backUpOption,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
@@ -98,9 +109,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (!isAutoBackup)
|
if (!isAutoBackup)
|
||||||
const Text(
|
const Text(
|
||||||
"Turn on backup to automatically upload new assets to the server.",
|
"backup_controller_page_desc_backup",
|
||||||
style: TextStyle(fontSize: 14),
|
style: TextStyle(fontSize: 14),
|
||||||
),
|
).tr(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
@@ -121,8 +132,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
.setAutoBackup(true);
|
.setAutoBackup(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text("Turn $backupBtnText Backup",
|
child: Text(
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
backupBtnText,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -132,13 +145,13 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSelectedAlbumName() {
|
Widget _buildSelectedAlbumName() {
|
||||||
var text = "Selected: ";
|
var text = "backup_controller_page_backup_selected".tr();
|
||||||
var albums = ref.watch(backupProvider).selectedBackupAlbums;
|
var albums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
|
|
||||||
if (albums.isNotEmpty) {
|
if (albums.isNotEmpty) {
|
||||||
for (var album in albums) {
|
for (var album in albums) {
|
||||||
if (album.name == "Recent" || album.name == "Recents") {
|
if (album.name == "Recent" || album.name == "Recents") {
|
||||||
text += "${album.name} (All), ";
|
text += "${album.name} (${'backup_all'.tr()}), ";
|
||||||
} else {
|
} else {
|
||||||
text += "${album.name}, ";
|
text += "${album.name}, ";
|
||||||
}
|
}
|
||||||
@@ -149,27 +162,29 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"None selected",
|
"backup_controller_page_none_selected".tr(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildExcludedAlbumName() {
|
Widget _buildExcludedAlbumName() {
|
||||||
var text = "Excluded: ";
|
var text = "backup_controller_page_excluded".tr();
|
||||||
var albums = ref.watch(backupProvider).excludedBackupAlbums;
|
var albums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
|
|
||||||
if (albums.isNotEmpty) {
|
if (albums.isNotEmpty) {
|
||||||
@@ -182,9 +197,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.red[300],
|
color: Colors.red[300],
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -205,17 +221,19 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
borderOnForeground: false,
|
borderOnForeground: false,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
minVerticalPadding: 15,
|
minVerticalPadding: 15,
|
||||||
title: const Text("Backup Albums",
|
title: const Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
"backup_controller_page_albums",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
|
).tr(),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
"Albums to be backed up",
|
"backup_controller_page_to_backup",
|
||||||
style: TextStyle(color: Color(0xFF808080), fontSize: 12),
|
style: TextStyle(color: Color(0xFF808080), fontSize: 12),
|
||||||
),
|
).tr(),
|
||||||
_buildSelectedAlbumName(),
|
_buildSelectedAlbumName(),
|
||||||
_buildExcludedAlbumName()
|
_buildExcludedAlbumName()
|
||||||
],
|
],
|
||||||
@@ -232,63 +250,215 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
|
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
|
||||||
},
|
},
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 16.0,
|
vertical: 16.0,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Select",
|
"backup_controller_page_select",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildCurrentBackupAssetInfoCard() {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.info_outline_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"backup_controller_page_uploading_file_info",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
|
).tr(),
|
||||||
|
if (ref.watch(errorBackupListProvider).isNotEmpty)
|
||||||
|
ActionChip(
|
||||||
|
avatar: Icon(
|
||||||
|
Icons.info,
|
||||||
|
size: 24,
|
||||||
|
color: Colors.red[400],
|
||||||
|
),
|
||||||
|
elevation: 1,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
label: Text(
|
||||||
|
"backup_controller_page_failed",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red[400],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
).tr(
|
||||||
|
args: [ref.watch(errorBackupListProvider).length.toString()],
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).push(const FailedBackupStatusRoute());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: LinearPercentIndicator(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
|
barRadius: const Radius.circular(2),
|
||||||
|
lineHeight: 10.0,
|
||||||
|
trailing: Text(
|
||||||
|
" ${backupState.progressInPercentage.toStringAsFixed(0)}%",
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
percent: backupState.progressInPercentage / 100.0,
|
||||||
|
backgroundColor: Colors.grey,
|
||||||
|
progressColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Table(
|
||||||
|
border: TableBorder.all(
|
||||||
|
color: Colors.black12,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: const Text(
|
||||||
|
'backup_controller_page_filename',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 10.0,
|
||||||
|
),
|
||||||
|
).tr(
|
||||||
|
args: [
|
||||||
|
backupState.currentUploadAsset.fileName,
|
||||||
|
backupState.currentUploadAsset.fileType
|
||||||
|
.toLowerCase()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: const Text(
|
||||||
|
"backup_controller_page_created",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 10.0,
|
||||||
|
),
|
||||||
|
).tr(
|
||||||
|
args: [
|
||||||
|
DateFormat.yMMMMd('en_US').format(
|
||||||
|
DateTime.parse(
|
||||||
|
backupState.currentUploadAsset.createdAt
|
||||||
|
.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: const Text(
|
||||||
|
"backup_controller_page_id",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 10.0,
|
||||||
|
),
|
||||||
|
).tr(args: [backupState.currentUploadAsset.id]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startBackup() {
|
||||||
|
ref.watch(errorBackupListProvider.notifier).empty();
|
||||||
|
ref.watch(backupProvider.notifier).startBackupProcess();
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Backup",
|
"backup_controller_page_backup",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
||||||
AutoRouter.of(context).pop(true);
|
AutoRouter.of(context).pop(true);
|
||||||
},
|
},
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.arrow_back_ios_rounded,
|
Icons.arrow_back_ios_rounded,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Backup Information",
|
"backup_controller_page_info",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
_buildFolderSelectionTile(),
|
_buildFolderSelectionTile(),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Total",
|
title: "backup_controller_page_total".tr(),
|
||||||
subtitle: "All unique photos and videos from selected albums",
|
subtitle: "backup_controller_page_total_sub".tr(),
|
||||||
info: "${backupState.allUniqueAssets.length}",
|
info: "${backupState.allUniqueAssets.length}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Backup",
|
title: "backup_controller_page_backup".tr(),
|
||||||
subtitle: "Backed up photos and videos",
|
subtitle: "backup_controller_page_backup_sub".tr(),
|
||||||
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Remainder",
|
title: "backup_controller_page_remainder".tr(),
|
||||||
subtitle: "Remaining photos and albums to back up from selection",
|
subtitle: "backup_controller_page_remainder_sub".tr(),
|
||||||
info:
|
info:
|
||||||
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
@@ -297,23 +467,11 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
_buildStorageInformation(),
|
_buildStorageInformation(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
_buildCurrentBackupAssetInfoCard(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.only(
|
||||||
child: Text(
|
top: 24,
|
||||||
"Asset that were being backup: ${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length} [${backupState.progressInPercentage.toStringAsFixed(0)}%]"),
|
),
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
|
||||||
child: Row(children: [
|
|
||||||
const Text("Backup Progress:"),
|
|
||||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 2)),
|
|
||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
|
||||||
? const CircularProgressIndicator.adaptive()
|
|
||||||
: const Text("Done"),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
child:
|
child:
|
||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
@@ -321,25 +479,33 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: Colors.red[300],
|
primary: Colors.red[300],
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
},
|
},
|
||||||
child: const Text("Cancel"),
|
child: const Text(
|
||||||
|
"backup_controller_page_cancel",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
)
|
)
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: Theme.of(context).primaryColor,
|
primary: Theme.of(context).primaryColor,
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
),
|
),
|
||||||
onPressed: shouldBackup
|
onPressed: shouldBackup ? startBackup : null,
|
||||||
? () {
|
child: const Text(
|
||||||
ref
|
"backup_controller_page_start_backup",
|
||||||
.read(backupProvider.notifier)
|
style: TextStyle(
|
||||||
.startBackupProcess();
|
fontSize: 14,
|
||||||
}
|
fontWeight: FontWeight.bold,
|
||||||
: null,
|
),
|
||||||
child: const Text("Start Backup"),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
141
mobile/lib/modules/backup/views/failed_backup_status_page.dart
Normal file
141
mobile/lib/modules/backup/views/failed_backup_status_page.dart
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class FailedBackupStatusPage extends HookConsumerWidget {
|
||||||
|
const FailedBackupStatusPage({Key? key}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final errorBackupList = ref.watch(errorBackupListProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
title: Text(
|
||||||
|
"Failed Backup (${errorBackupList.length})",
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).pop(true);
|
||||||
|
},
|
||||||
|
splashRadius: 24,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_ios_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: errorBackupList.length,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
var errorAsset = errorBackupList.elementAt(index);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12.0,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.black12,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 100,
|
||||||
|
minHeight: 150,
|
||||||
|
maxWidth: 100,
|
||||||
|
maxHeight: 200,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(15),
|
||||||
|
topLeft: Radius.circular(15),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: Image(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: AssetEntityImageProvider(
|
||||||
|
errorAsset.asset,
|
||||||
|
isOriginal: false,
|
||||||
|
thumbnailSize: const ThumbnailSize.square(512),
|
||||||
|
thumbnailFormat: ThumbnailFormat.jpeg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat.yMMMMd('en_US').format(
|
||||||
|
DateTime.parse(
|
||||||
|
errorAsset.createdAt.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: Colors.red.withAlpha(200),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Text(
|
||||||
|
errorAsset.fileName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
errorAsset.errorMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class DeleteAssetResponse {
|
|
||||||
final String id;
|
|
||||||
final String status;
|
|
||||||
|
|
||||||
DeleteAssetResponse({
|
|
||||||
required this.id,
|
|
||||||
required this.status,
|
|
||||||
});
|
|
||||||
|
|
||||||
DeleteAssetResponse copyWith({
|
|
||||||
String? id,
|
|
||||||
String? status,
|
|
||||||
}) {
|
|
||||||
return DeleteAssetResponse(
|
|
||||||
id: id ?? this.id,
|
|
||||||
status: status ?? this.status,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'status': status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory DeleteAssetResponse.fromMap(Map<String, dynamic> map) {
|
|
||||||
return DeleteAssetResponse(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
status: map['status'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory DeleteAssetResponse.fromJson(String source) =>
|
|
||||||
DeleteAssetResponse.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'DeleteAssetResponse(id: $id, status: $status)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is DeleteAssetResponse &&
|
|
||||||
other.id == id &&
|
|
||||||
other.status == status;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => id.hashCode ^ status.hashCode;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImmichAssetGroupByDate {
|
class ImmichAssetGroupByDate {
|
||||||
final String date;
|
final String date;
|
||||||
List<ImmichAsset> assets;
|
List<AssetResponseDto> assets;
|
||||||
ImmichAssetGroupByDate({
|
ImmichAssetGroupByDate({
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.assets,
|
required this.assets,
|
||||||
@@ -13,7 +11,7 @@ class ImmichAssetGroupByDate {
|
|||||||
|
|
||||||
ImmichAssetGroupByDate copyWith({
|
ImmichAssetGroupByDate copyWith({
|
||||||
String? date,
|
String? date,
|
||||||
List<ImmichAsset>? assets,
|
List<AssetResponseDto>? assets,
|
||||||
}) {
|
}) {
|
||||||
return ImmichAssetGroupByDate(
|
return ImmichAssetGroupByDate(
|
||||||
date: date ?? this.date,
|
date: date ?? this.date,
|
||||||
@@ -21,26 +19,6 @@ class ImmichAssetGroupByDate {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'date': date,
|
|
||||||
'assets': assets.map((x) => x.toMap()).toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ImmichAssetGroupByDate.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ImmichAssetGroupByDate(
|
|
||||||
date: map['date'] ?? '',
|
|
||||||
assets: List<ImmichAsset>.from(
|
|
||||||
map['assets']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ImmichAssetGroupByDate.fromJson(String source) =>
|
|
||||||
ImmichAssetGroupByDate.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
|
String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
|
||||||
|
|
||||||
@@ -79,28 +57,6 @@ class GetAllAssetResponse {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'count': count,
|
|
||||||
'data': data.map((x) => x.toMap()).toList(),
|
|
||||||
'nextPageKey': nextPageKey,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory GetAllAssetResponse.fromMap(Map<String, dynamic> map) {
|
|
||||||
return GetAllAssetResponse(
|
|
||||||
count: map['count']?.toInt() ?? 0,
|
|
||||||
data: List<ImmichAssetGroupByDate>.from(
|
|
||||||
map['data']?.map((x) => ImmichAssetGroupByDate.fromMap(x))),
|
|
||||||
nextPageKey: map['nextPageKey'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory GetAllAssetResponse.fromJson(String source) =>
|
|
||||||
GetAllAssetResponse.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
|
'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class HomePageState {
|
class HomePageState {
|
||||||
final bool isMultiSelectEnable;
|
final bool isMultiSelectEnable;
|
||||||
final Set<ImmichAsset> selectedItems;
|
final Set<AssetResponseDto> selectedItems;
|
||||||
final Set<String> selectedDateGroup;
|
final Set<String> selectedDateGroup;
|
||||||
HomePageState({
|
HomePageState({
|
||||||
required this.isMultiSelectEnable,
|
required this.isMultiSelectEnable,
|
||||||
@@ -16,7 +14,7 @@ class HomePageState {
|
|||||||
|
|
||||||
HomePageState copyWith({
|
HomePageState copyWith({
|
||||||
bool? isMultiSelectEnable,
|
bool? isMultiSelectEnable,
|
||||||
Set<ImmichAsset>? selectedItems,
|
Set<AssetResponseDto>? selectedItems,
|
||||||
Set<String>? selectedDateGroup,
|
Set<String>? selectedDateGroup,
|
||||||
}) {
|
}) {
|
||||||
return HomePageState(
|
return HomePageState(
|
||||||
@@ -26,28 +24,6 @@ class HomePageState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'isMultiSelectEnable': isMultiSelectEnable,
|
|
||||||
'selectedItems': selectedItems.map((x) => x.toMap()).toList(),
|
|
||||||
'selectedDateGroup': selectedDateGroup.toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory HomePageState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return HomePageState(
|
|
||||||
isMultiSelectEnable: map['isMultiSelectEnable'] ?? false,
|
|
||||||
selectedItems: Set<ImmichAsset>.from(
|
|
||||||
map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedDateGroup: Set<String>.from(map['selectedDateGroup']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory HomePageState.fromJson(String source) =>
|
|
||||||
HomePageState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
|
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user