Compare commits
17 Commits
v1.12.0_18
...
v1.13.0_20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
568436f188 | ||
|
|
04b59318f9 | ||
|
|
1a3d05ffc3 | ||
|
|
2f2db74d73 | ||
|
|
ef097d15dd | ||
|
|
caaa474c23 | ||
|
|
63bebd92e0 | ||
|
|
ad36b8b10f | ||
|
|
18c22d2a6c | ||
|
|
73024edba9 | ||
|
|
a360c0a3d7 | ||
|
|
34657f820f | ||
|
|
8840911f22 | ||
|
|
4aa66f4156 | ||
|
|
799a1c99f2 | ||
|
|
c4247bfea3 | ||
|
|
1e3464fe47 |
17
.github/workflows/test.yml
vendored
Normal file
17
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Test
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push: { branches: master }
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-server-e2e:
|
||||||
|
name: Run test suite
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Run Immich Server 2E2 Test
|
||||||
|
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich_server_test
|
||||||
57
README.md
57
README.md
@@ -8,7 +8,7 @@
|
|||||||
<img src="https://img.shields.io/teamcity/http/immichci.little-home.net/s/Immich_BuildAndPublishIOSToTestFlight.svg?style=for-the-badge&label=iOS&logo=teamcity&logoColor=000000&labelColor=ececec" alt="iOS Build"/>
|
<img src="https://img.shields.io/teamcity/http/immichci.little-home.net/s/Immich_BuildAndPublishIOSToTestFlight.svg?style=for-the-badge&label=iOS&logo=teamcity&logoColor=000000&labelColor=ececec" alt="iOS Build"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://actions-badge.atrox.dev/alextran1502/immich/goto?ref=main">
|
<a href="https://actions-badge.atrox.dev/alextran1502/immich/goto?ref=main">
|
||||||
<img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Falextran1502%2Fimmich%2Fbadge%3Fref%3Dmain&style=for-the-badge&label=Server Docker&logo=docker&labelColor=ececec" />
|
<img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Falextran1502%2Fimmich%2Fbadge%3Fref%3Dmain&style=for-the-badge&label=Github Action&logo=github&labelColor=ececec&logoColor=000000" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/rxnyVTXGbM">
|
<a href="https://discord.gg/rxnyVTXGbM">
|
||||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Immich%20Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Immich Discord"/>
|
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Immich%20Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Immich Discord"/>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
# Immich
|
# Immich
|
||||||
|
|
||||||
Self-hosted photo and video backup solution directly from your mobile phone.
|
**High performance self-hosted photo and video backup solution.**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ Loading ~4000 images/videos
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
### Mobile client
|
### Mobile
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/login-screen.png" width="150" title="Login With Custom URL">
|
<img src="design/login-screen.png" width="150" title="Login With Custom URL">
|
||||||
<img src="design/backup-screen.png" width="150" title="Backup Setting Info">
|
<img src="design/backup-screen.png" width="150" title="Backup Setting Info">
|
||||||
@@ -44,9 +44,10 @@ Loading ~4000 images/videos
|
|||||||
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
### Web client
|
### Web
|
||||||
<p align="center">
|
<p align="left">
|
||||||
<img src="design/dashboard_photos.jpeg" width="100%" title="Home Dashboard">
|
<img src="design/web-home.jpeg" width="49%" title="Home Dashboard">
|
||||||
|
<img src="design/web-detail.jpeg" width="49%" title="Detail">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Note
|
# Note
|
||||||
@@ -55,26 +56,22 @@ Loading ~4000 images/videos
|
|||||||
|
|
||||||
This project is under heavy development, there will be continuous functions, features and api changes.
|
This project is under heavy development, there will be continuous functions, features and api changes.
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
|
| | Mobile | Web |
|
||||||
|
| - | - | - |
|
||||||
|
| Upload and view videos and photos | Yes | Yes
|
||||||
|
| Auto backup when app is opened | Yes | N/A
|
||||||
|
| Selective album(s) for backup | Yes | N/A
|
||||||
|
| Download photos and videos to local device | Yes | Yes
|
||||||
|
| Multi-user support | Yes | Yes
|
||||||
|
| Shared Albums | Yes | No
|
||||||
|
| Quick navigation with draggable scrollbar | Yes | Yes
|
||||||
|
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
|
||||||
|
| Metadata view (EXIF, map) | Yes | Yes
|
||||||
|
| Search by metadata, objects and image tags | Yes | No
|
||||||
|
| Administrative functions (user management) | No | Yes
|
||||||
|
|
||||||
- Upload and view assets (videos/images).
|
|
||||||
- Auto Backup.
|
|
||||||
- Download asset to local device.
|
|
||||||
- Multi-user supported.
|
|
||||||
- Quick navigation with drag scroll bar.
|
|
||||||
- Support HEIC/HEIF Backup.
|
|
||||||
- Extract and display EXIF info.
|
|
||||||
- Real-time render from multi-device upload event.
|
|
||||||
- Image Tagging/Classification based on ImageNet dataset
|
|
||||||
- Object detection based on COCO SSD.
|
|
||||||
- Search assets based on tags and exif data (lens, make, model, orientation)
|
|
||||||
- [Optional] Reverse geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
|
||||||
- Show asset's location information on map (OpenStreetMap).
|
|
||||||
- Show curated places on the search page
|
|
||||||
- Show curated objects on the search page
|
|
||||||
- Shared album with users on the same server
|
|
||||||
- Selective backup - albums can be included and excluded during the backup process.
|
|
||||||
- Web interface is available for administrative tasks (creating new users) and viewing assets on the server - additional features are coming.
|
|
||||||
|
|
||||||
# System Requirement
|
# System Requirement
|
||||||
|
|
||||||
@@ -97,7 +94,7 @@ You can use docker compose for development and testing out the application, ther
|
|||||||
3. **PostgreSQL** - Main database of the application
|
3. **PostgreSQL** - Main database of the application
|
||||||
4. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
|
4. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
|
||||||
5. **Nginx** - Load balancing and optimized file uploading.
|
5. **Nginx** - Load balancing and optimized file uploading.
|
||||||
6. **TensorFlow** - Object Detection and Image Classification.
|
6. **TensorFlow** - Object Detection (COCO SSD) and Image Classification (ImageNet).
|
||||||
|
|
||||||
## Step 1: Populate .env file
|
## Step 1: Populate .env file
|
||||||
|
|
||||||
@@ -146,8 +143,8 @@ MAPBOX_KEY=
|
|||||||
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
||||||
# know where can it make the request to.
|
# know where can it make the request to.
|
||||||
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
||||||
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283
|
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
|
||||||
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283
|
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283/api
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 2: Start the server
|
## Step 2: Start the server
|
||||||
@@ -170,11 +167,11 @@ To *update* docker-compose with newest image (if you have started the docker-com
|
|||||||
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
The server will be running at `http://your-ip:2283` through `Nginx`
|
The server will be running at `http://your-ip:2283/api` through `Nginx`
|
||||||
|
|
||||||
## Step 3: Register User
|
## Step 3: Register User
|
||||||
|
|
||||||
Access the web interface at `http://your-ip:2285` to register an admin account.
|
Access the web interface at `http://your-ip:2283` to register an admin account.
|
||||||
|
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 154 KiB |
BIN
design/web-admin.jpeg
Normal file
BIN
design/web-admin.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
design/web-detail.jpeg
Normal file
BIN
design/web-detail.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
BIN
design/web-home.jpeg
Normal file
BIN
design/web-home.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
@@ -57,7 +57,7 @@ MAPBOX_KEY=
|
|||||||
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
||||||
# know where can it make the request to.
|
# know where can it make the request to.
|
||||||
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
||||||
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283
|
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
|
||||||
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
|
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
|
||||||
|
|
||||||
VITE_SERVER_ENDPOINT=
|
VITE_SERVER_ENDPOINT=
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ ENABLE_MAPBOX=false
|
|||||||
|
|
||||||
# WEB
|
# WEB
|
||||||
MAPBOX_KEY=
|
MAPBOX_KEY=
|
||||||
VITE_SERVER_ENDPOINT=http://localhost:2283
|
VITE_SERVER_ENDPOINT=http://localhost:2283/api
|
||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
image: immich-server-dev:1.9.0
|
image: immich-server-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -20,11 +20,9 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
image: immich-machine-learning-dev:1.9.0
|
image: immich-machine-learning-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../machine-learning
|
context: ../machine-learning
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -41,11 +39,9 @@ services:
|
|||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
image: immich-microservices:1.9.0
|
image: immich-microservices:latest
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -60,8 +56,7 @@ services:
|
|||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
- immich-server
|
||||||
- immich-network
|
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
image: immich-web-dev:1.9.0
|
image: immich-web-dev:1.9.0
|
||||||
@@ -73,20 +68,16 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 3002:3002
|
- 3002:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ../web:/usr/src/app
|
- ../web:/usr/src/app
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -102,8 +93,6 @@ services:
|
|||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
container_name: proxy_nginx
|
container_name: proxy_nginx
|
||||||
@@ -115,12 +104,8 @@ services:
|
|||||||
- 2284:443
|
- 2284:443
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-server
|
- immich-server
|
||||||
|
|
||||||
networks:
|
|
||||||
immich-network:
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
immich-server:
|
|
||||||
image: immich-server-dev:1.9.0
|
|
||||||
build:
|
|
||||||
context: ../server
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
command: npm run start:dev
|
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
volumes:
|
|
||||||
- ../server:/usr/src/app
|
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
- /usr/src/app/node_modules
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- database
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
immich-microservices:
|
|
||||||
image: immich-microservices-dev:1.9.0
|
|
||||||
build:
|
|
||||||
context: ../microservices
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
command: npm run start:dev
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: 1
|
|
||||||
capabilities: [ gpu ]
|
|
||||||
expose:
|
|
||||||
- "3001"
|
|
||||||
volumes:
|
|
||||||
- ../microservices:/usr/src/app
|
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
- /usr/src/app/node_modules
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
- database
|
|
||||||
- immich_server
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
redis:
|
|
||||||
container_name: immich_redis
|
|
||||||
image: redis:6.2
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
database:
|
|
||||||
container_name: immich_postgres
|
|
||||||
image: postgres:14
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
|
||||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
|
||||||
PG_DATA: /var/lib/postgresql/data
|
|
||||||
volumes:
|
|
||||||
- pgdata:/var/lib/postgresql/data
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
|
|
||||||
nginx:
|
|
||||||
container_name: proxy_nginx
|
|
||||||
image: nginx:latest
|
|
||||||
volumes:
|
|
||||||
- ./settings/nginx-conf:/etc/nginx/conf.d
|
|
||||||
ports:
|
|
||||||
- 2283:80
|
|
||||||
- 2284:443
|
|
||||||
logging:
|
|
||||||
driver: none
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
depends_on:
|
|
||||||
- immich-server
|
|
||||||
|
|
||||||
networks:
|
|
||||||
immich-network:
|
|
||||||
volumes:
|
|
||||||
pgdata:
|
|
||||||
@@ -4,8 +4,6 @@ services:
|
|||||||
immich-server:
|
immich-server:
|
||||||
image: altran1502/immich-server:staging
|
image: altran1502/immich-server:staging
|
||||||
entrypoint: ["/bin/sh", "./start-server.sh"]
|
entrypoint: ["/bin/sh", "./start-server.sh"]
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
@@ -15,8 +13,6 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
@@ -31,15 +27,11 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
image: altran1502/immich-machine-learning:staging
|
image: altran1502/immich-machine-learning:staging
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
|
||||||
- "3001"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
@@ -48,8 +40,6 @@ services:
|
|||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
@@ -57,17 +47,11 @@ services:
|
|||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
|
||||||
- 2285:3000
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
@@ -82,10 +66,6 @@ services:
|
|||||||
PG_DATA: /var/lib/postgresql/data
|
PG_DATA: /var/lib/postgresql/data
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
@@ -98,13 +78,9 @@ services:
|
|||||||
- 2284:443
|
- 2284:443
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-server
|
- immich-server
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
networks:
|
|
||||||
immich-network:
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server_test:
|
immich_server_test:
|
||||||
image: immich-server-dev:1.9.0
|
image: immich-server-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -19,15 +19,10 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich_network_test
|
|
||||||
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis_test
|
container_name: immich_redis_test
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
|
||||||
- immich_network_test
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres_test
|
container_name: immich_postgres_test
|
||||||
@@ -43,8 +38,3 @@ services:
|
|||||||
- /var/lib/postgresql/data
|
- /var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
|
||||||
- immich_network_test
|
|
||||||
|
|
||||||
networks:
|
|
||||||
immich_network_test:
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ services:
|
|||||||
immich-server:
|
immich-server:
|
||||||
image: altran1502/immich-server:latest
|
image: altran1502/immich-server:latest
|
||||||
entrypoint: ["/bin/sh", "./start-server.sh"]
|
entrypoint: ["/bin/sh", "./start-server.sh"]
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
@@ -15,8 +13,6 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
@@ -31,15 +27,11 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
image: altran1502/immich-machine-learning:latest
|
image: altran1502/immich-machine-learning:latest
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
|
||||||
- "3001"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
@@ -48,8 +40,6 @@ services:
|
|||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
@@ -57,17 +47,11 @@ services:
|
|||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
|
||||||
- 2285:3000
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
@@ -82,10 +66,6 @@ services:
|
|||||||
PG_DATA: /var/lib/postgresql/data
|
PG_DATA: /var/lib/postgresql/data
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
@@ -98,13 +78,9 @@ services:
|
|||||||
- 2284:443
|
- 2284:443
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
|
||||||
- immich-network
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-server
|
- immich-server
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
networks:
|
|
||||||
immich-network:
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
@@ -19,6 +19,33 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
gzip_static on;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_comp_level 2;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_buffer_size 16k;
|
||||||
|
proxy_busy_buffers_size 24k;
|
||||||
|
proxy_buffers 64 4k;
|
||||||
|
proxy_force_ranges on;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
|
rewrite /api/(.*) /$1 break;
|
||||||
|
|
||||||
|
proxy_pass http://immich-server:3000;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|
||||||
# Compression
|
# Compression
|
||||||
@@ -41,6 +68,6 @@ server {
|
|||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
proxy_pass http://immich-server:3000;
|
proxy_pass http://immich-web:3000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,45 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
#
|
#
|
||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b
|
revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
channel: stable
|
channel: stable
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: android
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: ios
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: linux
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: macos
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: web
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
- platform: windows
|
||||||
|
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ linter:
|
|||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
use_build_context_synchronously: false
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|||||||
@@ -81,5 +81,4 @@ flutter {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
// Generated file.
|
|
||||||
//
|
|
||||||
// If you wish to remove Flutter's multidex support, delete this entire file.
|
|
||||||
//
|
|
||||||
// Modifications to this file should be done in a copy under a different name
|
|
||||||
// as this file may be regenerated.
|
|
||||||
|
|
||||||
package io.flutter.app;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.CallSuper;
|
|
||||||
import androidx.multidex.MultiDex;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension of {@link android.app.Application}, adding multidex support.
|
|
||||||
*/
|
|
||||||
public class FlutterMultiDexApplication extends Application {
|
|
||||||
@Override
|
|
||||||
@CallSuper
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
MultiDex.install(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Added zoom functionality to the image viewer
|
||||||
|
* Fixed issue with the user is logged out after turning off the app
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Fixed WebSocket endpoint to confirm with the new settings on the server
|
||||||
@@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||||
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583
|
distributionSha256Sum=cd5c2958a107ee7f0722004a12d0f8559b4564c34daad7df06cffd4d12a426d0
|
||||||
@@ -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.12.0"
|
version_number: "1.13.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -2,18 +2,17 @@
|
|||||||
const String userInfoBox = "immichBoxUserInfo"; // Box
|
const String userInfoBox = "immichBoxUserInfo"; // Box
|
||||||
const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1
|
const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1
|
||||||
const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
|
const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
|
||||||
|
const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3
|
||||||
// Server endpoint
|
const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4
|
||||||
const String serverEndpointKey = 'immichBoxServerEndpoint';
|
|
||||||
|
|
||||||
// Login Info
|
// Login Info
|
||||||
const String hiveLoginInfoBox = "immichLoginInfoBox";
|
const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box
|
||||||
const String savedLoginInfoKey = "immichSavedLoginInfoKey";
|
const String savedLoginInfoKey = "immichSavedLoginInfoKey"; // Key 1
|
||||||
|
|
||||||
// Backup Info
|
// Backup Info
|
||||||
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox";
|
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox"; // Box
|
||||||
const String backupInfoKey = "immichBackupAlbumInfoKey";
|
const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1
|
||||||
|
|
||||||
// Github Release Info
|
// Github Release Info
|
||||||
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox";
|
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box
|
||||||
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey";
|
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
import '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 {
|
||||||
@@ -39,13 +40,14 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ImmichApp extends ConsumerStatefulWidget {
|
class ImmichApp extends ConsumerStatefulWidget {
|
||||||
const ImmichApp({Key? key}) : super(key: key);
|
const ImmichApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ImmichAppState createState() => _ImmichAppState();
|
ImmichAppState createState() => ImmichAppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserver {
|
class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
@@ -121,7 +123,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
primarySwatch: Colors.indigo,
|
primarySwatch: Colors.indigo,
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
snackBarTheme: const SnackBarThemeData(contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
|
||||||
scaffoldBackgroundColor: immichBackgroundColor,
|
scaffoldBackgroundColor: immichBackgroundColor,
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: immichBackgroundColor,
|
backgroundColor: immichBackgroundColor,
|
||||||
@@ -132,7 +135,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
routeInformationParser: _immichRouter.defaultRouteParser(),
|
routeInformationParser: _immichRouter.defaultRouteParser(),
|
||||||
routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
|
routerDelegate: _immichRouter.delegate(
|
||||||
|
navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
|
||||||
),
|
),
|
||||||
const ImmichLoadingOverlay(),
|
const ImmichLoadingOverlay(),
|
||||||
const VersionAnnouncementOverlay(),
|
const VersionAnnouncementOverlay(),
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import 'package:latlong2/latlong.dart';
|
|||||||
class ExifBottomSheet extends ConsumerWidget {
|
class ExifBottomSheet extends ConsumerWidget {
|
||||||
final ImmichAssetWithExif assetDetail;
|
final ImmichAssetWithExif assetDetail;
|
||||||
|
|
||||||
const ExifBottomSheet({Key? key, required this.assetDetail}) : super(key: key);
|
const ExifBottomSheet({Key? key, required this.assetDetail})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
_buildMap() {
|
_buildMap() {
|
||||||
return (assetDetail.exifInfo!.latitude != null && assetDetail.exifInfo!.longitude != null)
|
return (assetDetail.exifInfo!.latitude != null &&
|
||||||
|
assetDetail.exifInfo!.longitude != null)
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -25,12 +27,14 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
center: LatLng(assetDetail.exifInfo!.latitude!, assetDetail.exifInfo!.longitude!),
|
center: LatLng(assetDetail.exifInfo!.latitude!,
|
||||||
|
assetDetail.exifInfo!.longitude!),
|
||||||
zoom: 16.0,
|
zoom: 16.0,
|
||||||
),
|
),
|
||||||
layers: [
|
layers: [
|
||||||
TileLayerOptions(
|
TileLayerOptions(
|
||||||
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
urlTemplate:
|
||||||
|
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
subdomains: ['a', 'b', 'c'],
|
subdomains: ['a', 'b', 'c'],
|
||||||
attributionBuilder: (_) {
|
attributionBuilder: (_) {
|
||||||
return const Text(
|
return const Text(
|
||||||
@@ -43,8 +47,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
markers: [
|
markers: [
|
||||||
Marker(
|
Marker(
|
||||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
point: LatLng(assetDetail.exifInfo!.latitude!, assetDetail.exifInfo!.longitude!),
|
point: LatLng(assetDetail.exifInfo!.latitude!,
|
||||||
builder: (ctx) => const Image(image: AssetImage('assets/location-pin.png')),
|
assetDetail.exifInfo!.longitude!),
|
||||||
|
builder: (ctx) => const Image(
|
||||||
|
image: AssetImage('assets/location-pin.png')),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -56,10 +62,14 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildLocationText() {
|
_buildLocationText() {
|
||||||
return (assetDetail.exifInfo!.city != null && assetDetail.exifInfo!.state != null)
|
return (assetDetail.exifInfo!.city != null &&
|
||||||
|
assetDetail.exifInfo!.state != null)
|
||||||
? Text(
|
? Text(
|
||||||
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[200], fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
)
|
)
|
||||||
: Container();
|
: Container();
|
||||||
}
|
}
|
||||||
@@ -131,7 +141,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"DETAILS",
|
"DETAILS",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
style:
|
||||||
|
TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -158,7 +169,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
leading: const Icon(Icons.camera),
|
leading: const Icon(Icons.camera),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
|
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
|
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
|
||||||
|
|||||||
113
mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart
Normal file
113
mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
|
enum _RemoteImageStatus { empty, thumbnail, full }
|
||||||
|
|
||||||
|
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
|
late CachedNetworkImageProvider _imageProvider;
|
||||||
|
_RemoteImageStatus _status = _RemoteImageStatus.empty;
|
||||||
|
bool _zoomedIn = false;
|
||||||
|
|
||||||
|
static const int swipeThreshold = 100;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool allowMoving = _status == _RemoteImageStatus.full;
|
||||||
|
|
||||||
|
return PhotoView(
|
||||||
|
imageProvider: _imageProvider,
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
||||||
|
enablePanAlways: true,
|
||||||
|
scaleStateChangedCallback: _scaleStateChanged,
|
||||||
|
onScaleEnd: _onScaleListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScaleListener(BuildContext context, ScaleEndDetails details,
|
||||||
|
PhotoViewControllerValue controllerValue) {
|
||||||
|
// Disable swipe events when zoomed in
|
||||||
|
if (_zoomedIn) return;
|
||||||
|
|
||||||
|
if (controllerValue.position.dy > swipeThreshold) {
|
||||||
|
widget.onSwipeDown();
|
||||||
|
} else if (controllerValue.position.dy < -swipeThreshold) {
|
||||||
|
widget.onSwipeUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scaleStateChanged(PhotoViewScaleState state) {
|
||||||
|
_zoomedIn = state == PhotoViewScaleState.zoomedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
||||||
|
return CachedNetworkImageProvider(url,
|
||||||
|
headers: {"Authorization": widget.authToken}, cacheKey: url);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _performStateTransition(
|
||||||
|
_RemoteImageStatus newStatus, CachedNetworkImageProvider provider) {
|
||||||
|
// Transition to same status is forbidden
|
||||||
|
if (_status == newStatus) return;
|
||||||
|
// Transition full -> thumbnail is forbidden
|
||||||
|
if (_status == _RemoteImageStatus.full &&
|
||||||
|
newStatus == _RemoteImageStatus.thumbnail) return;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_status = newStatus;
|
||||||
|
_imageProvider = provider;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadImages() {
|
||||||
|
CachedNetworkImageProvider thumbnailProvider =
|
||||||
|
_authorizedImageProvider(widget.thumbnailUrl);
|
||||||
|
_imageProvider = thumbnailProvider;
|
||||||
|
|
||||||
|
thumbnailProvider
|
||||||
|
.resolve(const ImageConfiguration())
|
||||||
|
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
|
_performStateTransition(_RemoteImageStatus.thumbnail, thumbnailProvider);
|
||||||
|
}));
|
||||||
|
|
||||||
|
CachedNetworkImageProvider fullProvider =
|
||||||
|
_authorizedImageProvider(widget.imageUrl);
|
||||||
|
fullProvider
|
||||||
|
.resolve(const ImageConfiguration())
|
||||||
|
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
|
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_loadImages();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemotePhotoView extends StatefulWidget {
|
||||||
|
const RemotePhotoView(
|
||||||
|
{Key? key,
|
||||||
|
required this.thumbnailUrl,
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.authToken,
|
||||||
|
required this.onSwipeDown,
|
||||||
|
required this.onSwipeUp})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
final String thumbnailUrl;
|
||||||
|
final String imageUrl;
|
||||||
|
final String authToken;
|
||||||
|
|
||||||
|
final void Function() onSwipeDown;
|
||||||
|
final void Function() onSwipeUp;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _RemotePhotoViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -5,7 +7,10 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|||||||
|
|
||||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
const TopControlAppBar(
|
const TopControlAppBar(
|
||||||
{Key? key, required this.asset, required this.onMoreInfoPressed, required this.onDownloadPressed})
|
{Key? key,
|
||||||
|
required this.asset,
|
||||||
|
required this.onMoreInfoPressed,
|
||||||
|
required this.onDownloadPressed})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
final ImmichAsset asset;
|
final ImmichAsset asset;
|
||||||
@@ -42,9 +47,11 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
print("favorite");
|
log("favorite");
|
||||||
},
|
},
|
||||||
icon: asset.isFavorite ? const Icon(Icons.favorite_rounded) : const Icon(Icons.favorite_border_rounded),
|
icon: asset.isFavorite
|
||||||
|
? const Icon(Icons.favorite_rounded)
|
||||||
|
: const Icon(Icons.favorite_border_rounded),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
@@ -10,6 +8,7 @@ import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_stat
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
@@ -63,64 +62,19 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SwipeDetector(
|
body: SafeArea(
|
||||||
onSwipeDown: (_) {
|
|
||||||
AutoRouter.of(context).pop();
|
|
||||||
},
|
|
||||||
onSwipeUp: (_) {
|
|
||||||
showInfo();
|
|
||||||
},
|
|
||||||
child: SafeArea(
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: CachedNetworkImage(
|
child: RemotePhotoView(
|
||||||
fit: BoxFit.cover,
|
thumbnailUrl: thumbnailUrl,
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
authToken: "Bearer ${box.get(accessTokenKey)}",
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
errorWidget: (context, url, error) => ConstrainedBox(
|
onSwipeUp: () => showInfo(),
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
)
|
||||||
child: Wrap(
|
|
||||||
spacing: 32,
|
|
||||||
runSpacing: 32,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"Failed To Render Image - Possibly Corrupted Data",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
|
||||||
),
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: Text(
|
|
||||||
error.toString(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
placeholder: (context, url) {
|
|
||||||
return CachedNetworkImage(
|
|
||||||
cacheKey: thumbnailUrl,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: thumbnailUrl,
|
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
|
||||||
placeholderFadeInDuration: const Duration(milliseconds: 0),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) => Icon(
|
|
||||||
Icons.error,
|
|
||||||
color: Colors.grey[300],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
@@ -130,7 +84,6 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
@@ -55,7 +54,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
removeExcludedAlbumForBackup(album);
|
removeExcludedAlbumForBackup(album);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
|
state = state
|
||||||
|
.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
|
||||||
_updateBackupAssetCount();
|
_updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
if (state.selectedBackupAlbums.contains(album)) {
|
if (state.selectedBackupAlbums.contains(album)) {
|
||||||
removeAlbumForBackup(album);
|
removeAlbumForBackup(album);
|
||||||
}
|
}
|
||||||
state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
|
state = state
|
||||||
|
.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
|
||||||
_updateBackupAssetCount();
|
_updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,16 +95,19 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
Future<void> getBackupAlbumsInfo() async {
|
Future<void> getBackupAlbumsInfo() async {
|
||||||
// Get all albums on the device
|
// Get all albums on the device
|
||||||
List<AvailableAlbum> availableAlbums = [];
|
List<AvailableAlbum> availableAlbums = [];
|
||||||
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(hasAll: true, type: RequestType.common);
|
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
||||||
|
hasAll: true, type: RequestType.common);
|
||||||
|
|
||||||
for (AssetPathEntity album in albums) {
|
for (AssetPathEntity album in albums) {
|
||||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||||
|
|
||||||
var assetList = await album.getAssetListRange(start: 0, end: album.assetCount);
|
var assetList =
|
||||||
|
await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
|
|
||||||
if (assetList.isNotEmpty) {
|
if (assetList.isNotEmpty) {
|
||||||
var thumbnailAsset = assetList.first;
|
var thumbnailAsset = assetList.first;
|
||||||
var thumbnailData = await thumbnailAsset.thumbnailDataWithSize(const ThumbnailSize(512, 512));
|
var thumbnailData = await thumbnailAsset
|
||||||
|
.thumbnailDataWithSize(const ThumbnailSize(512, 512));
|
||||||
availableAlbum = availableAlbum.copyWith(thumbnailData: thumbnailData);
|
availableAlbum = availableAlbum.copyWith(thumbnailData: thumbnailData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +118,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Put persistent storage info into local state of the app
|
// Put persistent storage info into local state of the app
|
||||||
// Get local storage on selected backup album
|
// Get local storage on selected backup album
|
||||||
Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
Box<HiveBackupAlbums> backupAlbumInfoBox =
|
||||||
|
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get(
|
HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get(
|
||||||
backupInfoKey,
|
backupInfoKey,
|
||||||
defaultValue: HiveBackupAlbums(
|
defaultValue: HiveBackupAlbums(
|
||||||
@@ -133,7 +138,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
debugPrint("First time backup setup recent album as default");
|
debugPrint("First time backup setup recent album as default");
|
||||||
|
|
||||||
// Get album that contains all assets
|
// Get album that contains all assets
|
||||||
var list = await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
|
var list = await PhotoManager.getAssetPathList(
|
||||||
|
hasAll: true, onlyAll: true, type: RequestType.common);
|
||||||
AssetPathEntity albumHasAllAssets = list.first;
|
AssetPathEntity albumHasAllAssets = list.first;
|
||||||
|
|
||||||
backupAlbumInfoBox.put(
|
backupAlbumInfoBox.put(
|
||||||
@@ -151,12 +157,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
try {
|
try {
|
||||||
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
||||||
state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
|
state = state.copyWith(
|
||||||
|
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
||||||
state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
|
state = state.copyWith(
|
||||||
|
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] Failed to generate album from id $e");
|
debugPrint("[ERROR] Failed to generate album from id $e");
|
||||||
@@ -173,21 +181,27 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
||||||
|
|
||||||
for (var album in state.selectedBackupAlbums) {
|
for (var album in state.selectedBackupAlbums) {
|
||||||
var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
|
var assets =
|
||||||
|
await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
assetsFromSelectedAlbums.addAll(assets);
|
assetsFromSelectedAlbums.addAll(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var album in state.excludedBackupAlbums) {
|
for (var album in state.excludedBackupAlbums) {
|
||||||
var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
|
var assets =
|
||||||
|
await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
assetsFromExcludedAlbums.addAll(assets);
|
assetsFromExcludedAlbums.addAll(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<AssetEntity> allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
Set<AssetEntity> allUniqueAssets =
|
||||||
List<String> allAssetOnDatabase = await _backupService.getDeviceBackupAsset();
|
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
||||||
|
List<String> allAssetOnDatabase =
|
||||||
|
await _backupService.getDeviceBackupAsset();
|
||||||
|
|
||||||
// Find asset that were backup from selected albums
|
// Find asset that were backup from selected albums
|
||||||
Set<String> selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.id));
|
Set<String> selectedAlbumsBackupAssets =
|
||||||
selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
|
Set.from(allUniqueAssets.map((e) => e.id));
|
||||||
|
selectedAlbumsBackupAssets
|
||||||
|
.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
|
||||||
|
|
||||||
if (allUniqueAssets.isEmpty) {
|
if (allUniqueAssets.isEmpty) {
|
||||||
debugPrint("No Asset On Device");
|
debugPrint("No Asset On Device");
|
||||||
@@ -226,7 +240,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// Hive database
|
/// Hive database
|
||||||
///
|
///
|
||||||
void _updatePersistentAlbumsSelection() {
|
void _updatePersistentAlbumsSelection() {
|
||||||
Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
Box<HiveBackupAlbums> backupAlbumInfoBox =
|
||||||
|
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
backupAlbumInfoBox.put(
|
backupAlbumInfoBox.put(
|
||||||
backupInfoKey,
|
backupInfoKey,
|
||||||
HiveBackupAlbums(
|
HiveBackupAlbums(
|
||||||
@@ -268,7 +283,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Perform Backup
|
// Perform Backup
|
||||||
state = state.copyWith(cancelToken: CancellationToken());
|
state = state.copyWith(cancelToken: CancellationToken());
|
||||||
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken, _onAssetUploaded, _onUploadProgress);
|
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken,
|
||||||
|
_onAssetUploaded, _onUploadProgress);
|
||||||
} else {
|
} else {
|
||||||
PhotoManager.openSetting();
|
PhotoManager.openSetting();
|
||||||
}
|
}
|
||||||
@@ -276,23 +292,32 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
void cancelBackup() {
|
void cancelBackup() {
|
||||||
state.cancelToken.cancel();
|
state.cancelToken.cancel();
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
state = state.copyWith(
|
||||||
|
backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(selectedAlbumsBackupAssetsIds: {
|
||||||
selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, deviceAssetId},
|
...state.selectedAlbumsBackupAssetsIds,
|
||||||
allAssetOnDatabase: [...state.allAssetOnDatabase, deviceAssetId]);
|
deviceAssetId
|
||||||
|
}, allAssetOnDatabase: [
|
||||||
|
...state.allAssetOnDatabase,
|
||||||
|
deviceAssetId
|
||||||
|
]);
|
||||||
|
|
||||||
if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) {
|
if (state.allUniqueAssets.length -
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
|
state.selectedAlbumsBackupAssetsIds.length ==
|
||||||
|
0) {
|
||||||
|
state = state.copyWith(
|
||||||
|
backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateServerInfo();
|
_updateServerInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onUploadProgress(int sent, int total) {
|
void _onUploadProgress(int sent, int total) {
|
||||||
state = state.copyWith(progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
|
state = state.copyWith(
|
||||||
|
progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateServerInfo() async {
|
void _updateServerInfo() async {
|
||||||
@@ -326,7 +351,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this device is enable backup by the user
|
// Check if this device is enable backup by the user
|
||||||
if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
|
if ((authState.deviceInfo.deviceId == authState.deviceId) &&
|
||||||
|
authState.deviceInfo.isAutoBackup) {
|
||||||
// check if backup is alreayd in process - then return
|
// check if backup is alreayd in process - then return
|
||||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||||
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
||||||
@@ -343,6 +369,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
final backupProvider =
|
||||||
|
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||||
return BackupNotifier(ref: ref);
|
return BackupNotifier(ref: ref);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,16 +17,21 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
BackUpState backupState = ref.watch(backupProvider);
|
BackUpState backupState = ref.watch(backupProvider);
|
||||||
AuthenticationState _authenticationState = ref.watch(authenticationProvider);
|
AuthenticationState authenticationState = ref.watch(authenticationProvider);
|
||||||
bool shouldBackup =
|
bool shouldBackup = backupState.allUniqueAssets.length -
|
||||||
backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 ? false : true;
|
backupState.selectedAlbumsBackupAssetsIds.length ==
|
||||||
|
0
|
||||||
|
? false
|
||||||
|
: true;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
||||||
ref.read(backupProvider.notifier).getBackupInfo();
|
ref.read(backupProvider.notifier).getBackupInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success');
|
ref
|
||||||
|
.watch(websocketProvider.notifier)
|
||||||
|
.stopListenToEvent('on_upload_success');
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -48,7 +53,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: LinearPercentIndicator(
|
child: LinearPercentIndicator(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
barRadius: const Radius.circular(2),
|
barRadius: const Radius.circular(2),
|
||||||
lineHeight: 6.0,
|
lineHeight: 6.0,
|
||||||
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
||||||
@@ -58,7 +64,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 12.0),
|
padding: const EdgeInsets.only(top: 12.0),
|
||||||
child: Text('${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'),
|
child: Text(
|
||||||
|
'${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -67,9 +74,11 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ListTile _buildBackupController() {
|
ListTile _buildBackupController() {
|
||||||
var backUpOption = _authenticationState.deviceInfo.isAutoBackup ? "on" : "off";
|
var backUpOption =
|
||||||
var isAutoBackup = _authenticationState.deviceInfo.isAutoBackup;
|
authenticationState.deviceInfo.isAutoBackup ? "on" : "off";
|
||||||
var backupBtnText = _authenticationState.deviceInfo.isAutoBackup ? "off" : "on";
|
var isAutoBackup = authenticationState.deviceInfo.isAutoBackup;
|
||||||
|
var backupBtnText =
|
||||||
|
authenticationState.deviceInfo.isAutoBackup ? "off" : "on";
|
||||||
return ListTile(
|
return ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
leading: isAutoBackup
|
leading: isAutoBackup
|
||||||
@@ -104,10 +113,15 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
isAutoBackup
|
isAutoBackup
|
||||||
? ref.watch(authenticationProvider.notifier).setAutoBackup(false)
|
? ref
|
||||||
: ref.watch(authenticationProvider.notifier).setAutoBackup(true);
|
.watch(authenticationProvider.notifier)
|
||||||
|
.setAutoBackup(false)
|
||||||
|
: ref
|
||||||
|
.watch(authenticationProvider.notifier)
|
||||||
|
.setAutoBackup(true);
|
||||||
},
|
},
|
||||||
child: Text("Turn $backupBtnText Backup", style: const TextStyle(fontWeight: FontWeight.bold)),
|
child: Text("Turn $backupBtnText Backup",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -133,7 +147,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -141,7 +158,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"None selected",
|
"None selected",
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -160,7 +180,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(color: Colors.red[300], fontSize: 12, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.red[300],
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -181,7 +204,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
borderOnForeground: false,
|
borderOnForeground: false,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
minVerticalPadding: 15,
|
minVerticalPadding: 15,
|
||||||
title: const Text("Backup Albums", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
title: const Text("Backup Albums",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -258,13 +282,16 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Backup",
|
title: "Backup",
|
||||||
subtitle: "Photos and videos from selected albums that are backup",
|
subtitle:
|
||||||
|
"Photos and videos from selected albums that are backup",
|
||||||
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Remainder",
|
title: "Remainder",
|
||||||
subtitle: "Photos and videos that has not been backing up from selected albums",
|
subtitle:
|
||||||
info: "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
"Photos and videos that has not been backing up from selected albums",
|
||||||
|
info:
|
||||||
|
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
_buildBackupController(),
|
_buildBackupController(),
|
||||||
@@ -289,29 +316,32 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
child: backupState.backupProgress == BackUpProgressEnum.inProgress
|
child:
|
||||||
? ElevatedButton(
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
style: ElevatedButton.styleFrom(
|
? ElevatedButton(
|
||||||
primary: Colors.red[300],
|
style: ElevatedButton.styleFrom(
|
||||||
onPrimary: Colors.grey[50],
|
primary: Colors.red[300],
|
||||||
),
|
onPrimary: Colors.grey[50],
|
||||||
onPressed: () {
|
),
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
onPressed: () {
|
||||||
},
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
child: const Text("Cancel"),
|
},
|
||||||
)
|
child: const Text("Cancel"),
|
||||||
: ElevatedButton(
|
)
|
||||||
style: ElevatedButton.styleFrom(
|
: ElevatedButton(
|
||||||
primary: Theme.of(context).primaryColor,
|
style: ElevatedButton.styleFrom(
|
||||||
onPrimary: Colors.grey[50],
|
primary: Theme.of(context).primaryColor,
|
||||||
),
|
onPrimary: Colors.grey[50],
|
||||||
onPressed: shouldBackup
|
),
|
||||||
? () {
|
onPressed: shouldBackup
|
||||||
ref.read(backupProvider.notifier).startBackupProcess();
|
? () {
|
||||||
}
|
ref
|
||||||
: null,
|
.read(backupProvider.notifier)
|
||||||
child: const Text("Start Backup"),
|
.startBackupProcess();
|
||||||
),
|
}
|
||||||
|
: null,
|
||||||
|
child: const Text("Start Backup"),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
|
||||||
|
|
||||||
class DisableMultiSelectButton extends ConsumerWidget {
|
class DisableMultiSelectButton extends ConsumerWidget {
|
||||||
const DisableMultiSelectButton({
|
const DisableMultiSelectButton({
|
||||||
@@ -36,7 +35,8 @@ class DisableMultiSelectButton extends ConsumerWidget {
|
|||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
selectedItemCount.toString(),
|
selectedItemCount.toString(),
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600, fontSize: 18),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
this.labelTextBuilder,
|
this.labelTextBuilder,
|
||||||
this.labelConstraints,
|
this.labelConstraints,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical),
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
scrollThumbBuilder = _thumbRRectBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
|
scrollThumbBuilder =
|
||||||
|
_thumbRRectBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
DraggableScrollbar.arrows({
|
DraggableScrollbar.arrows({
|
||||||
@@ -98,7 +99,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
this.labelTextBuilder,
|
this.labelTextBuilder,
|
||||||
this.labelConstraints,
|
this.labelConstraints,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical),
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
scrollThumbBuilder = _thumbArrowBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
|
scrollThumbBuilder =
|
||||||
|
_thumbArrowBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
DraggableScrollbar.semicircle({
|
DraggableScrollbar.semicircle({
|
||||||
@@ -115,11 +117,12 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
this.labelTextBuilder,
|
this.labelTextBuilder,
|
||||||
this.labelConstraints,
|
this.labelConstraints,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical),
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb),
|
scrollThumbBuilder = _thumbSemicircleBuilder(
|
||||||
|
heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_DraggableScrollbarState createState() => _DraggableScrollbarState();
|
DraggableScrollbarState createState() => DraggableScrollbarState();
|
||||||
|
|
||||||
static buildScrollThumbAndLabel(
|
static buildScrollThumbAndLabel(
|
||||||
{required Widget scrollThumb,
|
{required Widget scrollThumb,
|
||||||
@@ -137,9 +140,9 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
children: [
|
children: [
|
||||||
ScrollLabel(
|
ScrollLabel(
|
||||||
animation: labelAnimation,
|
animation: labelAnimation,
|
||||||
child: labelText,
|
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
constraints: labelConstraints,
|
constraints: labelConstraints,
|
||||||
|
child: labelText,
|
||||||
),
|
),
|
||||||
scrollThumb,
|
scrollThumb,
|
||||||
],
|
],
|
||||||
@@ -154,7 +157,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
static ScrollThumbBuilder _thumbSemicircleBuilder(
|
||||||
|
double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
@@ -168,9 +172,6 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
foregroundPainter: ArrowCustomPainter(Colors.white),
|
foregroundPainter: ArrowCustomPainter(Colors.white),
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints.tight(Size(width, height)),
|
|
||||||
),
|
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(height),
|
topLeft: Radius.circular(height),
|
||||||
@@ -178,6 +179,9 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
topRight: const Radius.circular(4.0),
|
topRight: const Radius.circular(4.0),
|
||||||
bottomRight: const Radius.circular(4.0),
|
bottomRight: const Radius.circular(4.0),
|
||||||
),
|
),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints.tight(Size(width, height)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -193,7 +197,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbArrowBuilder(Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
static ScrollThumbBuilder _thumbArrowBuilder(
|
||||||
|
Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
@@ -203,6 +208,7 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
BoxConstraints? labelConstraints,
|
BoxConstraints? labelConstraints,
|
||||||
}) {
|
}) {
|
||||||
final scrollThumb = ClipPath(
|
final scrollThumb = ClipPath(
|
||||||
|
clipper: ArrowClipper(),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: height,
|
height: height,
|
||||||
width: 20.0,
|
width: 20.0,
|
||||||
@@ -213,7 +219,6 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
clipper: ArrowClipper(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return buildScrollThumbAndLabel(
|
return buildScrollThumbAndLabel(
|
||||||
@@ -228,7 +233,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbRRectBuilder(Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
static ScrollThumbBuilder _thumbRRectBuilder(
|
||||||
|
Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
@@ -239,13 +245,13 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
}) {
|
}) {
|
||||||
final scrollThumb = Material(
|
final scrollThumb = Material(
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints.tight(
|
constraints: BoxConstraints.tight(
|
||||||
Size(16.0, height),
|
Size(16.0, height),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
color: backgroundColor,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return buildScrollThumbAndLabel(
|
return buildScrollThumbAndLabel(
|
||||||
@@ -267,7 +273,8 @@ class ScrollLabel extends StatelessWidget {
|
|||||||
final Text child;
|
final Text child;
|
||||||
|
|
||||||
final BoxConstraints? constraints;
|
final BoxConstraints? constraints;
|
||||||
static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0);
|
static const BoxConstraints _defaultConstraints =
|
||||||
|
BoxConstraints.tightFor(width: 72.0, height: 28.0);
|
||||||
|
|
||||||
const ScrollLabel({
|
const ScrollLabel({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -298,7 +305,8 @@ class ScrollLabel extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
|
class DraggableScrollbarState extends State<DraggableScrollbar>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
late double _barOffset;
|
late double _barOffset;
|
||||||
late double _viewOffset;
|
late double _viewOffset;
|
||||||
late bool _isDragInProcess;
|
late bool _isDragInProcess;
|
||||||
@@ -345,7 +353,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb;
|
double get barMaxScrollExtent =>
|
||||||
|
context.size!.height - widget.heightScrollThumb;
|
||||||
|
|
||||||
double get barMinScrollExtent => 0;
|
double get barMinScrollExtent => 0;
|
||||||
|
|
||||||
@@ -362,7 +371,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
return LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
//print("LayoutBuilder constraints=$constraints");
|
//print("LayoutBuilder constraints=$constraints");
|
||||||
|
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
@@ -432,7 +442,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification is ScrollUpdateNotification || notification is OverscrollNotification) {
|
if (notification is ScrollUpdateNotification ||
|
||||||
|
notification is OverscrollNotification) {
|
||||||
if (_thumbAnimationController.status != AnimationStatus.forward) {
|
if (_thumbAnimationController.status != AnimationStatus.forward) {
|
||||||
_thumbAnimationController.forward();
|
_thumbAnimationController.forward();
|
||||||
}
|
}
|
||||||
@@ -486,7 +497,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||||||
_barOffset = barMaxScrollExtent;
|
_barOffset = barMaxScrollExtent;
|
||||||
}
|
}
|
||||||
|
|
||||||
double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
|
double viewDelta = getScrollViewDelta(
|
||||||
|
details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
|
||||||
|
|
||||||
_viewOffset = widget.controller.position.pixels + viewDelta;
|
_viewOffset = widget.controller.position.pixels + viewDelta;
|
||||||
if (_viewOffset < widget.controller.position.minScrollExtent) {
|
if (_viewOffset < widget.controller.position.minScrollExtent) {
|
||||||
@@ -566,7 +578,8 @@ class ArrowClipper extends CustomClipper<Path> {
|
|||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
|
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY);
|
path.lineTo(startPointX + arrowWidth, startPointY);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
|
path.lineTo(
|
||||||
|
startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
|
||||||
path.lineTo(startPointX, startPointY + 1.0);
|
path.lineTo(startPointX, startPointY + 1.0);
|
||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
@@ -575,7 +588,8 @@ class ArrowClipper extends CustomClipper<Path> {
|
|||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
|
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
|
||||||
path.lineTo(startPointX, startPointY);
|
path.lineTo(startPointX, startPointY);
|
||||||
path.lineTo(startPointX, startPointY - 1.0);
|
path.lineTo(startPointX, startPointY - 1.0);
|
||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
|
path.lineTo(
|
||||||
|
startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
@@ -600,7 +614,8 @@ class SlideFadeTransition extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: animation,
|
animation: animation,
|
||||||
builder: (context, child) => animation.value == 0.0 ? Container() : child!,
|
builder: (context, child) =>
|
||||||
|
animation.value == 0.0 ? Container() : child!,
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
position: Tween(
|
position: Tween(
|
||||||
begin: const Offset(0.3, 0.0),
|
begin: const Offset(0.3, 0.0),
|
||||||
|
|||||||
@@ -20,16 +20,18 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final BackUpState _backupState = ref.watch(backupProvider);
|
final BackUpState backupState = ref.watch(backupProvider);
|
||||||
bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
bool isEnableAutoBackup =
|
||||||
final ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
||||||
|
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
floating: true,
|
floating: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
snap: false,
|
snap: false,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||||
leading: Builder(
|
leading: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
@@ -47,7 +49,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_serverInfoState.isVersionMismatch
|
serverInfoState.isVersionMismatch
|
||||||
? Positioned(
|
? Positioned(
|
||||||
bottom: 12,
|
bottom: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
@@ -88,7 +90,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
Stack(
|
Stack(
|
||||||
alignment: AlignmentDirectional.center,
|
alignment: AlignmentDirectional.center,
|
||||||
children: [
|
children: [
|
||||||
_backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
? Positioned(
|
? Positioned(
|
||||||
top: 10,
|
top: 10,
|
||||||
right: 12,
|
right: 12,
|
||||||
@@ -97,7 +99,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
width: 8,
|
width: 8,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Theme.of(context).primaryColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -105,7 +108,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
iconSize: 30,
|
iconSize: 30,
|
||||||
icon: _isEnableAutoBackup
|
icon: isEnableAutoBackup
|
||||||
? const Icon(Icons.backup_rounded)
|
? const Icon(Icons.backup_rounded)
|
||||||
: Badge(
|
: Badge(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
@@ -118,20 +121,23 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: const Icon(Icons.backup_rounded)),
|
child: const Icon(Icons.backup_rounded)),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
|
var onPop = await AutoRouter.of(context)
|
||||||
|
.push(const BackupControllerRoute());
|
||||||
|
|
||||||
if (onPop != null && onPop == true) {
|
if (onPop != null && onPop == true) {
|
||||||
onPopBack!();
|
onPopBack!();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
? Positioned(
|
? Positioned(
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Text(
|
child: Text(
|
||||||
(_backupState.allUniqueAssets.length - _backupState.selectedAlbumsBackupAssetsIds.length)
|
(backupState.allUniqueAssets.length -
|
||||||
|
backupState.selectedAlbumsBackupAssetsIds.length)
|
||||||
.toString(),
|
.toString(),
|
||||||
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 9, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Container()
|
: Container()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
@@ -23,9 +24,10 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
AuthenticationState _authState = ref.watch(authenticationProvider);
|
AuthenticationState authState = ref.watch(authenticationProvider);
|
||||||
ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
||||||
final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status;
|
final uploadProfileImageStatus =
|
||||||
|
ref.watch(uploadProfileImageProvider).status;
|
||||||
final appInfo = useState({});
|
final appInfo = useState({});
|
||||||
var dummmy = Random().nextInt(1024);
|
var dummmy = Random().nextInt(1024);
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildUserProfileImage() {
|
_buildUserProfileImage() {
|
||||||
if (_authState.profileImagePath.isEmpty) {
|
if (authState.profileImagePath.isEmpty) {
|
||||||
return const CircleAvatar(
|
return const CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
@@ -48,10 +50,11 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
||||||
if (_authState.profileImagePath.isNotEmpty) {
|
if (authState.profileImagePath.isNotEmpty) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
|
backgroundImage: NetworkImage(
|
||||||
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -66,7 +69,8 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
|
backgroundImage: NetworkImage(
|
||||||
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,15 +91,16 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pickUserProfileImage() async {
|
_pickUserProfileImage() async {
|
||||||
final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
|
final XFile? image = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
var success =
|
||||||
|
await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
ref
|
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
|
||||||
.watch(authenticationProvider.notifier)
|
ref.read(uploadProfileImageProvider).profileImagePath);
|
||||||
.updateUserProfileImagePath(ref.read(uploadProfileImageProvider).profileImagePath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +121,10 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
DrawerHeader(
|
DrawerHeader(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [Color.fromARGB(255, 216, 219, 238), Color.fromARGB(255, 226, 230, 231)],
|
colors: [
|
||||||
|
Color.fromARGB(255, 216, 219, 238),
|
||||||
|
Color.fromARGB(255, 226, 230, 231)
|
||||||
|
],
|
||||||
begin: Alignment.centerRight,
|
begin: Alignment.centerRight,
|
||||||
end: Alignment.centerLeft,
|
end: Alignment.centerLeft,
|
||||||
),
|
),
|
||||||
@@ -154,7 +162,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"${_authState.firstName} ${_authState.lastName}",
|
"${authState.firstName} ${authState.lastName}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -162,7 +170,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_authState.userEmail,
|
authState.userEmail,
|
||||||
style: TextStyle(color: Colors.grey[800], fontSize: 12),
|
style: TextStyle(color: Colors.grey[800], fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -176,16 +184,21 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Sign Out",
|
"Sign Out",
|
||||||
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.black54,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
bool res = await ref.read(authenticationProvider.notifier).logout();
|
bool res =
|
||||||
|
await ref.read(authenticationProvider.notifier).logout();
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
ref.watch(backupProvider.notifier).cancelBackup();
|
ref.watch(backupProvider.notifier).cancelBackup();
|
||||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
ref.watch(websocketProvider.notifier).disconnect();
|
ref.watch(websocketProvider.notifier).disconnect();
|
||||||
AutoRouter.of(context).popUntilRoot();
|
// AutoRouter.of(context).popUntilRoot();
|
||||||
|
AutoRouter.of(context).replace(const LoginRoute());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -204,19 +217,22 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
_serverInfoState.isVersionMismatch
|
serverInfoState.isVersionMismatch
|
||||||
? _serverInfoState.versionMismatchErrorMessage
|
? serverInfoState.versionMismatchErrorMessage
|
||||||
: "Client and Server are up-to-date",
|
: "Client and Server are up-to-date",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style:
|
style: TextStyle(
|
||||||
TextStyle(fontSize: 11, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w600),
|
fontSize: 11,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@@ -254,7 +270,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"${_serverInfoState.serverVersion.major}.${_serverInfoState.serverVersion.minor}.${_serverInfoState.serverVersion.patch}",
|
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ class HomePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
ScrollController _scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
||||||
List<Widget> _imageGridGroup = [];
|
List<Widget> imageGridGroup = [];
|
||||||
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
var isMultiSelectEnable =
|
||||||
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
var homePageState = ref.watch(homePageStateProvider);
|
var homePageState = ref.watch(homePageStateProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
@@ -39,7 +40,8 @@ class HomePage extends HookConsumerWidget {
|
|||||||
_buildSelectedItemCountIndicator() {
|
_buildSelectedItemCountIndicator() {
|
||||||
return isMultiSelectEnable
|
return isMultiSelectEnable
|
||||||
? DisableMultiSelectButton(
|
? DisableMultiSelectButton(
|
||||||
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
|
onPressed:
|
||||||
|
ref.watch(homePageStateProvider.notifier).disableMultiSelect,
|
||||||
selectedItemCount: homePageState.selectedItems.length,
|
selectedItemCount: homePageState.selectedItems.length,
|
||||||
)
|
)
|
||||||
: Container();
|
: Container();
|
||||||
@@ -59,7 +61,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (lastMonth != null) {
|
if (lastMonth != null) {
|
||||||
if (currentMonth - lastMonth! != 0) {
|
if (currentMonth - lastMonth! != 0) {
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
MonthlyTitleText(
|
MonthlyTitleText(
|
||||||
isoDate: dateGroup,
|
isoDate: dateGroup,
|
||||||
),
|
),
|
||||||
@@ -67,14 +69,14 @@ class HomePage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
DailyTitleText(
|
DailyTitleText(
|
||||||
isoDate: dateGroup,
|
isoDate: dateGroup,
|
||||||
assetGroup: immichAssetList,
|
assetGroup: immichAssetList,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
ImageGrid(assetGroup: immichAssetList),
|
ImageGrid(assetGroup: immichAssetList),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -109,12 +111,12 @@ class HomePage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 50.0),
|
padding: const EdgeInsets.only(top: 50.0),
|
||||||
child: DraggableScrollbar.semicircle(
|
child: DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
..._imageGridGroup,
|
...imageGridGroup,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
enableFeedback: true,
|
enableFeedback: true,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Save login",
|
"Stay logged in",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
),
|
),
|
||||||
value: isSaveLoginInfo.value,
|
value: isSaveLoginInfo.value,
|
||||||
|
|||||||
@@ -45,20 +45,23 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getSuggestedSearchTerms() async {
|
void getSuggestedSearchTerms() async {
|
||||||
var userSuggestedSearchTerms = await _searchService.getUserSuggestedSearchTerms();
|
var userSuggestedSearchTerms =
|
||||||
|
await _searchService.getUserSuggestedSearchTerms();
|
||||||
|
|
||||||
state = state.copyWith(userSuggestedSearchTerms: userSuggestedSearchTerms);
|
state = state.copyWith(userSuggestedSearchTerms: userSuggestedSearchTerms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
|
final searchPageStateProvider =
|
||||||
|
StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
|
||||||
return SearchPageStateNotifier();
|
return SearchPageStateNotifier();
|
||||||
});
|
});
|
||||||
|
|
||||||
final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
|
final getCuratedLocationProvider =
|
||||||
final SearchService _searchService = SearchService();
|
FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
|
||||||
|
final SearchService searchService = SearchService();
|
||||||
|
|
||||||
var curatedLocation = await _searchService.getCuratedLocation();
|
var curatedLocation = await searchService.getCuratedLocation();
|
||||||
if (curatedLocation != null) {
|
if (curatedLocation != null) {
|
||||||
return curatedLocation;
|
return curatedLocation;
|
||||||
} else {
|
} else {
|
||||||
@@ -66,10 +69,11 @@ final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocati
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final getCuratedObjectProvider = FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
|
final getCuratedObjectProvider =
|
||||||
final SearchService _searchService = SearchService();
|
FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
|
||||||
|
final SearchService searchService = SearchService();
|
||||||
|
|
||||||
var curatedObject = await _searchService.getCuratedObjects();
|
var curatedObject = await searchService.getCuratedObjects();
|
||||||
if (curatedObject != null) {
|
if (curatedObject != null) {
|
||||||
return curatedObject;
|
return curatedObject;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
|||||||
@@ -12,24 +12,27 @@ import 'package:immich_mobile/modules/search/providers/search_result_page.provid
|
|||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
|
|
||||||
class SearchResultPage extends HookConsumerWidget {
|
class SearchResultPage extends HookConsumerWidget {
|
||||||
SearchResultPage({Key? key, required this.searchTerm}) : super(key: key);
|
const SearchResultPage({Key? key, required this.searchTerm})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
final String searchTerm;
|
final String searchTerm;
|
||||||
late FocusNode searchFocusNode;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
ScrollController _scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
final searchTermController = useTextEditingController(text: "");
|
final searchTermController = useTextEditingController(text: "");
|
||||||
final isNewSearch = useState(false);
|
final isNewSearch = useState(false);
|
||||||
final currentSearchTerm = useState(searchTerm);
|
final currentSearchTerm = useState(searchTerm);
|
||||||
|
|
||||||
List<Widget> _imageGridGroup = [];
|
final List<Widget> imageGridGroup = [];
|
||||||
|
|
||||||
|
late FocusNode searchFocusNode;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
searchFocusNode = FocusNode();
|
searchFocusNode = FocusNode();
|
||||||
|
|
||||||
Future.delayed(Duration.zero, () => ref.read(searchResultPageProvider.notifier).search(searchTerm));
|
Future.delayed(Duration.zero,
|
||||||
|
() => ref.read(searchResultPageProvider.notifier).search(searchTerm));
|
||||||
return () => searchFocusNode.dispose();
|
return () => searchFocusNode.dispose();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -85,7 +88,10 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
currentSearchTerm.value,
|
currentSearchTerm.value,
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 13, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
@@ -124,7 +130,7 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (lastMonth != null) {
|
if (lastMonth != null) {
|
||||||
if (currentMonth - lastMonth! != 0) {
|
if (currentMonth - lastMonth! != 0) {
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
MonthlyTitleText(
|
MonthlyTitleText(
|
||||||
isoDate: dateGroup,
|
isoDate: dateGroup,
|
||||||
),
|
),
|
||||||
@@ -132,14 +138,14 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
DailyTitleText(
|
DailyTitleText(
|
||||||
isoDate: dateGroup,
|
isoDate: dateGroup,
|
||||||
assetGroup: immichAssetList,
|
assetGroup: immichAssetList,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
ImageGrid(assetGroup: immichAssetList),
|
ImageGrid(assetGroup: immichAssetList),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -148,11 +154,11 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return DraggableScrollbar.semicircle(
|
return DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
slivers: [..._imageGridGroup],
|
slivers: [...imageGridGroup],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -192,7 +198,9 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildSearchResult(),
|
_buildSearchResult(),
|
||||||
isNewSearch.value ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
|
isNewSearch.value
|
||||||
|
? SearchSuggestionList(onSubmitted: _onSearchSubmitted)
|
||||||
|
: Container(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
|||||||
final SharedAlbumService _sharedAlbumService = SharedAlbumService();
|
final SharedAlbumService _sharedAlbumService = SharedAlbumService();
|
||||||
|
|
||||||
getAllSharedAlbums() async {
|
getAllSharedAlbums() async {
|
||||||
List<SharedAlbum> sharedAlbums = await _sharedAlbumService.getAllSharedAlbum();
|
List<SharedAlbum> sharedAlbums =
|
||||||
|
await _sharedAlbumService.getAllSharedAlbum();
|
||||||
|
|
||||||
state = sharedAlbums;
|
state = sharedAlbums;
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,8 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(String albumId, List<String> assetIds) async {
|
Future<bool> removeAssetFromAlbum(
|
||||||
|
String albumId, List<String> assetIds) async {
|
||||||
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
|
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
@@ -46,12 +48,14 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final sharedAlbumProvider = StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
|
final sharedAlbumProvider =
|
||||||
|
StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
|
||||||
return SharedAlbumNotifier();
|
return SharedAlbumNotifier();
|
||||||
});
|
});
|
||||||
|
|
||||||
final sharedAlbumDetailProvider = FutureProvider.autoDispose.family<SharedAlbum, String>((ref, albumId) async {
|
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
||||||
final SharedAlbumService _sharedAlbumService = SharedAlbumService();
|
.family<SharedAlbum, String>((ref, albumId) async {
|
||||||
|
final SharedAlbumService sharedAlbumService = SharedAlbumService();
|
||||||
|
|
||||||
return await _sharedAlbumService.getAlbumDetail(albumId);
|
return await sharedAlbumService.getAlbumDetail(albumId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,18 +26,22 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
|
final isMultiSelectionEnable =
|
||||||
final selectedAssetsInAlbum = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
ref.watch(assetSelectionProvider).isMultiselectEnable;
|
||||||
|
final selectedAssetsInAlbum =
|
||||||
|
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||||
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||||
|
|
||||||
void _onDeleteAlbumPressed(String albumId) async {
|
void _onDeleteAlbumPressed(String albumId) async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
|
bool isSuccess =
|
||||||
|
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
|
AutoRouter.of(context)
|
||||||
|
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -53,10 +57,12 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
void _onLeaveAlbumPressed(String albumId) async {
|
void _onLeaveAlbumPressed(String albumId) async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(albumId);
|
bool isSuccess =
|
||||||
|
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
|
AutoRouter.of(context)
|
||||||
|
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -73,10 +79,11 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
void _onRemoveFromAlbumPressed(String albumId) async {
|
void _onRemoveFromAlbumPressed(String albumId) async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
bool isSuccess =
|
||||||
albumId,
|
await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
|
||||||
selectedAssetsInAlbum.map((a) => a.id).toList(),
|
albumId,
|
||||||
);
|
selectedAssetsInAlbum.map((a) => a.id).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -153,15 +160,18 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
_buildLeadingButton() {
|
_buildLeadingButton() {
|
||||||
if (isMultiSelectionEnable) {
|
if (isMultiSelectionEnable) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () => ref.watch(assetSelectionProvider.notifier).disableMultiselection(),
|
onPressed: () => ref
|
||||||
|
.watch(assetSelectionProvider.notifier)
|
||||||
|
.disableMultiselection(),
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
);
|
);
|
||||||
} else if (isEditAlbum) {
|
} else if (isEditAlbum) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
bool isSuccess =
|
bool isSuccess = await ref
|
||||||
await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(albumId, userId, newAlbumTitle);
|
.watch(albumViewerProvider.notifier)
|
||||||
|
.changeAlbumTitle(albumId, userId, newAlbumTitle);
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -187,7 +197,9 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
return AppBar(
|
return AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: _buildLeadingButton(),
|
leading: _buildLeadingButton(),
|
||||||
title: isMultiSelectionEnable ? Text(selectedAssetsInAlbum.length.toString()) : Container(),
|
title: isMultiSelectionEnable
|
||||||
|
? Text(selectedAssetsInAlbum.length.toString())
|
||||||
|
: Container(),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
FocusNode titleFocusNode = useFocusNode();
|
FocusNode titleFocusNode = useFocusNode();
|
||||||
ScrollController _scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
AsyncValue<SharedAlbum> _albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
|
AsyncValue<SharedAlbum> albumInfo =
|
||||||
|
ref.watch(sharedAlbumDetailProvider(albumId));
|
||||||
|
|
||||||
final userId = ref.watch(authenticationProvider).userId;
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
|
|
||||||
@@ -44,16 +45,16 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
||||||
|
|
||||||
AssetSelectionPageResult? returnPayload =
|
AssetSelectionPageResult? returnPayload = await AutoRouter.of(context)
|
||||||
await AutoRouter.of(context).push<AssetSelectionPageResult?>(const AssetSelectionRoute());
|
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
|
||||||
|
|
||||||
if (returnPayload != null) {
|
if (returnPayload != null) {
|
||||||
// Check if there is new assets add
|
// Check if there is new assets add
|
||||||
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
|
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
var isSuccess =
|
var isSuccess = await SharedAlbumService().addAdditionalAssetToAlbum(
|
||||||
await SharedAlbumService().addAdditionalAssetToAlbum(returnPayload.selectedAdditionalAsset, albumId);
|
returnPayload.selectedAdditionalAsset, albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
ref.refresh(sharedAlbumDetailProvider(albumId));
|
ref.refresh(sharedAlbumDetailProvider(albumId));
|
||||||
@@ -69,13 +70,15 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onAddUsersPressed(SharedAlbum albumInfo) async {
|
void _onAddUsersPressed(SharedAlbum albumInfo) async {
|
||||||
List<String>? sharedUserIds =
|
List<String>? sharedUserIds = await AutoRouter.of(context)
|
||||||
await AutoRouter.of(context).push<List<String>?>(SelectAdditionalUserForSharingRoute(albumInfo: albumInfo));
|
.push<List<String>?>(
|
||||||
|
SelectAdditionalUserForSharingRoute(albumInfo: albumInfo));
|
||||||
|
|
||||||
if (sharedUserIds != null) {
|
if (sharedUserIds != null) {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
var isSuccess = await SharedAlbumService().addAdditionalUserToAlbum(sharedUserIds, albumId);
|
var isSuccess = await SharedAlbumService()
|
||||||
|
.addAdditionalUserToAlbum(sharedUserIds, albumId);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
ref.refresh(sharedAlbumDetailProvider(albumId));
|
ref.refresh(sharedAlbumDetailProvider(albumId));
|
||||||
@@ -95,7 +98,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Text(albumInfo.albumName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
child: Text(albumInfo.albumName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24, fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -103,8 +108,10 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
Widget _buildAlbumDateRange(SharedAlbum albumInfo) {
|
Widget _buildAlbumDateRange(SharedAlbum albumInfo) {
|
||||||
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) {
|
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) {
|
||||||
String startDate = "";
|
String startDate = "";
|
||||||
DateTime parsedStartDate = DateTime.parse(albumInfo.assets!.first.createdAt);
|
DateTime parsedStartDate =
|
||||||
DateTime parsedEndDate = DateTime.parse(albumInfo.assets!.last.createdAt);
|
DateTime.parse(albumInfo.assets!.first.createdAt);
|
||||||
|
DateTime parsedEndDate =
|
||||||
|
DateTime.parse(albumInfo.assets!.last.createdAt);
|
||||||
|
|
||||||
if (parsedStartDate.year == parsedEndDate.year) {
|
if (parsedStartDate.year == parsedEndDate.year) {
|
||||||
startDate = DateFormat('LLL d').format(parsedStartDate);
|
startDate = DateFormat('LLL d').format(parsedStartDate);
|
||||||
@@ -118,7 +125,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(left: 16.0, top: 8),
|
padding: const EdgeInsets.only(left: 16.0, top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
"$startDate-$endDate",
|
"$startDate-$endDate",
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
|
style: const TextStyle(
|
||||||
|
fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -147,8 +155,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(2.0),
|
padding: const EdgeInsets.all(2.0),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
child: Image.asset('assets/immich-logo-no-outline.png'),
|
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
|
child:
|
||||||
|
Image.asset('assets/immich-logo-no-outline.png'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -217,10 +226,10 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: DraggableScrollbar.semicircle(
|
child: DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
_buildHeader(albumInfo),
|
_buildHeader(albumInfo),
|
||||||
SliverPersistentHeader(
|
SliverPersistentHeader(
|
||||||
@@ -242,8 +251,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AlbumViewerAppbar(albumInfo: _albumInfo, userId: userId, albumId: albumId),
|
appBar: AlbumViewerAppbar(
|
||||||
body: _albumInfo.when(
|
albumInfo: albumInfo, userId: userId, albumId: albumId),
|
||||||
|
body: albumInfo.when(
|
||||||
data: (albumInfo) => _buildBody(albumInfo),
|
data: (albumInfo) => _buildBody(albumInfo),
|
||||||
error: (e, _) => Center(child: Text("Error loading album info $e")),
|
error: (e, _) => Center(child: Text("Error loading album info $e")),
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ 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();
|
||||||
var assetGroupMonthYear = ref.watch(assetGroupByMonthYearProvider);
|
var assetGroupMonthYear = ref.watch(assetGroupByMonthYearProvider);
|
||||||
final selectedAssets = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
final selectedAssets =
|
||||||
final newAssetsForAlbum = ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||||
|
final newAssetsForAlbum =
|
||||||
|
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
||||||
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||||
|
|
||||||
List<Widget> _imageGridGroup = [];
|
List<Widget> imageGridGroup = [];
|
||||||
|
|
||||||
String _buildAssetCountText() {
|
String _buildAssetCountText() {
|
||||||
if (isAlbumExist) {
|
if (isAlbumExist) {
|
||||||
@@ -31,19 +33,20 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
assetGroupMonthYear.forEach((monthYear, assetGroup) {
|
assetGroupMonthYear.forEach((monthYear, assetGroup) {
|
||||||
_imageGridGroup.add(MonthGroupTitle(month: monthYear, assetGroup: assetGroup));
|
imageGridGroup
|
||||||
_imageGridGroup.add(AssetGridByMonth(assetGroup: assetGroup));
|
.add(MonthGroupTitle(month: monthYear, assetGroup: assetGroup));
|
||||||
|
imageGridGroup.add(AssetGridByMonth(assetGroup: assetGroup));
|
||||||
});
|
});
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
DraggableScrollbar.semicircle(
|
DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: scrollController,
|
||||||
slivers: [..._imageGridGroup],
|
slivers: [...imageGridGroup],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -71,7 +74,8 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
(!isAlbumExist && selectedAssets.isNotEmpty) || (isAlbumExist && newAssetsForAlbum.isNotEmpty)
|
(!isAlbumExist && selectedAssets.isNotEmpty) ||
|
||||||
|
(isAlbumExist && newAssetsForAlbum.isNotEmpty)
|
||||||
? TextButton(
|
? TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
var payload = AssetSelectionPageResult(
|
var payload = AssetSelectionPageResult(
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final albumTitleController = useTextEditingController.fromValue(TextEditingValue.empty);
|
final albumTitleController =
|
||||||
|
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
final albumTitleTextFieldFocusNode = useFocusNode();
|
final albumTitleTextFieldFocusNode = useFocusNode();
|
||||||
final isAlbumTitleTextFieldFocus = useState(false);
|
final isAlbumTitleTextFieldFocus = useState(false);
|
||||||
final isAlbumTitleEmpty = useState(true);
|
final isAlbumTitleEmpty = useState(true);
|
||||||
final selectedAssets = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
final selectedAssets =
|
||||||
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||||
|
|
||||||
_showSelectUserPage() {
|
_showSelectUserPage() {
|
||||||
AutoRouter.of(context).push(const SelectUserForSharingRoute());
|
AutoRouter.of(context).push(const SelectUserForSharingRoute());
|
||||||
@@ -38,8 +40,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
|||||||
_onSelectPhotosButtonPressed() async {
|
_onSelectPhotosButtonPressed() async {
|
||||||
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false);
|
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false);
|
||||||
|
|
||||||
AssetSelectionPageResult? selectedAsset =
|
AssetSelectionPageResult? selectedAsset = await AutoRouter.of(context)
|
||||||
await AutoRouter.of(context).push<AssetSelectionPageResult?>(const AssetSelectionRoute());
|
.push<AssetSelectionPageResult?>(const AssetSelectionRoute());
|
||||||
|
|
||||||
if (selectedAsset == null) {
|
if (selectedAsset == null) {
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||||
@@ -84,16 +86,22 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
|||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
padding:
|
||||||
side: const BorderSide(color: Color.fromARGB(255, 206, 206, 206)),
|
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))),
|
side: const BorderSide(
|
||||||
|
color: Color.fromARGB(255, 206, 206, 206)),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5))),
|
||||||
onPressed: _onSelectPhotosButtonPressed,
|
onPressed: _onSelectPhotosButtonPressed,
|
||||||
icon: const Icon(Icons.add_rounded),
|
icon: const Icon(Icons.add_rounded),
|
||||||
label: Padding(
|
label: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Select Photos',
|
'Select Photos',
|
||||||
style: TextStyle(fontSize: 16, color: Colors.grey[700], fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -141,7 +149,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
|||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _onBackgroundTapped,
|
onTap: _onBackgroundTapped,
|
||||||
child: SharedAlbumThumbnailImage(asset: selectedAssets.toList()[index]),
|
child: SharedAlbumThumbnailImage(
|
||||||
|
asset: selectedAssets.toList()[index]),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
childCount: selectedAssets.length,
|
childCount: selectedAssets.length,
|
||||||
@@ -169,7 +178,9 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: albumTitleController.text.isNotEmpty ? _showSelectUserPage : null,
|
onPressed: albumTitleController.text.isNotEmpty
|
||||||
|
? _showSelectUserPage
|
||||||
|
: null,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Share',
|
'Share',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -189,13 +200,13 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
|||||||
pinned: true,
|
pinned: true,
|
||||||
floating: false,
|
floating: false,
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(66.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildTitleInputField(),
|
_buildTitleInputField(),
|
||||||
_buildControlButton(),
|
_buildControlButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
preferredSize: const Size.fromHeight(66.0),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'package:immich_mobile/routing/auth_guard.dart';
|
|||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
|
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
||||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
@@ -27,8 +28,9 @@ part 'router.gr.dart';
|
|||||||
@MaterialAutoRouter(
|
@MaterialAutoRouter(
|
||||||
replaceInRouteName: 'Page,Route',
|
replaceInRouteName: 'Page,Route',
|
||||||
routes: <AutoRoute>[
|
routes: <AutoRoute>[
|
||||||
AutoRoute(page: LoginPage, initial: true),
|
AutoRoute(page: SplashScreenPage, initial: true),
|
||||||
AutoRoute(
|
AutoRoute(page: LoginPage),
|
||||||
|
CustomRoute(
|
||||||
page: TabControllerPage,
|
page: TabControllerPage,
|
||||||
guards: [AuthGuard],
|
guards: [AuthGuard],
|
||||||
children: [
|
children: [
|
||||||
@@ -36,6 +38,7 @@ part 'router.gr.dart';
|
|||||||
AutoRoute(page: SearchPage, guards: [AuthGuard]),
|
AutoRoute(page: SearchPage, guards: [AuthGuard]),
|
||||||
AutoRoute(page: SharingPage, guards: [AuthGuard])
|
AutoRoute(page: SharingPage, guards: [AuthGuard])
|
||||||
],
|
],
|
||||||
|
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||||
),
|
),
|
||||||
AutoRoute(page: ImageViewerPage, guards: [AuthGuard]),
|
AutoRoute(page: ImageViewerPage, guards: [AuthGuard]),
|
||||||
AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
|
AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
|
||||||
|
|||||||
@@ -21,13 +21,21 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
final Map<String, PageFactory> pagesMap = {
|
final Map<String, PageFactory> pagesMap = {
|
||||||
|
SplashScreenRoute.name: (routeData) {
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData, child: const SplashScreenPage());
|
||||||
|
},
|
||||||
LoginRoute.name: (routeData) {
|
LoginRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData, child: const LoginPage());
|
routeData: routeData, child: const LoginPage());
|
||||||
},
|
},
|
||||||
TabControllerRoute.name: (routeData) {
|
TabControllerRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return CustomPage<dynamic>(
|
||||||
routeData: routeData, child: const TabControllerPage());
|
routeData: routeData,
|
||||||
|
child: const TabControllerPage(),
|
||||||
|
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||||
|
opaque: true,
|
||||||
|
barrierDismissible: false);
|
||||||
},
|
},
|
||||||
ImageViewerRoute.name: (routeData) {
|
ImageViewerRoute.name: (routeData) {
|
||||||
final args = routeData.argsAs<ImageViewerRouteArgs>();
|
final args = routeData.argsAs<ImageViewerRouteArgs>();
|
||||||
@@ -121,7 +129,8 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<RouteConfig> get routes => [
|
List<RouteConfig> get routes => [
|
||||||
RouteConfig(LoginRoute.name, path: '/'),
|
RouteConfig(SplashScreenRoute.name, path: '/'),
|
||||||
|
RouteConfig(LoginRoute.name, path: '/login-page'),
|
||||||
RouteConfig(TabControllerRoute.name,
|
RouteConfig(TabControllerRoute.name,
|
||||||
path: '/tab-controller-page',
|
path: '/tab-controller-page',
|
||||||
guards: [
|
guards: [
|
||||||
@@ -167,10 +176,18 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [SplashScreenPage]
|
||||||
|
class SplashScreenRoute extends PageRouteInfo<void> {
|
||||||
|
const SplashScreenRoute() : super(SplashScreenRoute.name, path: '/');
|
||||||
|
|
||||||
|
static const String name = 'SplashScreenRoute';
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [LoginPage]
|
/// [LoginPage]
|
||||||
class LoginRoute extends PageRouteInfo<void> {
|
class LoginRoute extends PageRouteInfo<void> {
|
||||||
const LoginRoute() : super(LoginRoute.name, path: '/');
|
const LoginRoute() : super(LoginRoute.name, path: '/login-page');
|
||||||
|
|
||||||
static const String name = 'LoginRoute';
|
static const String name = 'LoginRoute';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ffi';
|
|
||||||
|
|
||||||
class DeviceInfoRemote {
|
class DeviceInfoRemote {
|
||||||
final int id;
|
final int id;
|
||||||
@@ -66,7 +65,8 @@ class DeviceInfoRemote {
|
|||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory DeviceInfoRemote.fromJson(String source) => DeviceInfoRemote.fromMap(json.decode(source));
|
factory DeviceInfoRemote.fromJson(String source) =>
|
||||||
|
DeviceInfoRemote.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
|
|||||||
deleteAssets(Set<ImmichAsset> deleteAssets) async {
|
deleteAssets(Set<ImmichAsset> deleteAssets) async {
|
||||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||||
var deviceId = deviceInfo["deviceId"];
|
var deviceId = deviceInfo["deviceId"];
|
||||||
List<String> deleteIdList = [];
|
var deleteIdList = <String>[];
|
||||||
// Delete asset from device
|
// Delete asset from device
|
||||||
for (var asset in deleteAssets) {
|
for (var asset in deleteAssets) {
|
||||||
// Delete asset on device if present
|
// Delete asset on device if present
|
||||||
if (asset.deviceId == deviceId) {
|
if (asset.deviceId == deviceId) {
|
||||||
AssetEntity? localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
||||||
|
|
||||||
if (localAsset != null) {
|
if (localAsset != null) {
|
||||||
deleteIdList.add(localAsset.id);
|
deleteIdList.add(localAsset.id);
|
||||||
@@ -46,37 +46,45 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<String> result = await PhotoManager.editor.deleteWithIds(deleteIdList);
|
// final List<String> result = await PhotoManager.editor.deleteWithIds(deleteIdList);
|
||||||
|
await PhotoManager.editor.deleteWithIds(deleteIdList);
|
||||||
|
|
||||||
// Delete asset on server
|
// Delete asset on server
|
||||||
List<DeleteAssetResponse>? deleteAssetResult = await _assetService.deleteAssets(deleteAssets);
|
List<DeleteAssetResponse>? deleteAssetResult =
|
||||||
|
await _assetService.deleteAssets(deleteAssets);
|
||||||
if (deleteAssetResult == null) {
|
if (deleteAssetResult == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var asset in deleteAssetResult) {
|
for (var asset in deleteAssetResult) {
|
||||||
if (asset.status == 'success') {
|
if (asset.status == 'success') {
|
||||||
state = state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
state =
|
||||||
|
state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetProvider = StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) {
|
final assetProvider =
|
||||||
|
StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) {
|
||||||
return AssetNotifier(ref);
|
return AssetNotifier(ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(assetProvider);
|
var assets = ref.watch(assetProvider);
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
assets.sortByCompare<DateTime>(
|
||||||
return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
|
(e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
||||||
|
return assets.groupListsBy((element) =>
|
||||||
|
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
|
||||||
});
|
});
|
||||||
|
|
||||||
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(assetProvider);
|
var assets = ref.watch(assetProvider);
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
assets.sortByCompare<DateTime>(
|
||||||
|
(e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
||||||
|
|
||||||
return assets.groupListsBy((element) => DateFormat('MMMM, y').format(DateTime.parse(element.createdAt)));
|
return assets.groupListsBy((element) =>
|
||||||
|
DateFormat('MMMM, y').format(DateTime.parse(element.createdAt)));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
|
|||||||
try {
|
try {
|
||||||
String? localReleaseVersion = box.get(githubReleaseInfoKey);
|
String? localReleaseVersion = box.get(githubReleaseInfoKey);
|
||||||
|
|
||||||
Response res = await dio.get(
|
var res = await dio.get(
|
||||||
"https://api.github.com/repos/alextran1502/immich/releases/latest",
|
"https://api.github.com/repos/alextran1502/immich/releases/latest",
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {"Accept": "application/vnd.github.v3+json"},
|
headers: {"Accept": "application/vnd.github.v3+json"},
|
||||||
@@ -34,7 +34,8 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (latestTagVersion.isNotEmpty && localReleaseVersion != latestTagVersion) {
|
if (latestTagVersion.isNotEmpty &&
|
||||||
|
localReleaseVersion != latestTagVersion) {
|
||||||
VersionAnnouncementOverlayController.appLoader.show();
|
VersionAnnouncementOverlayController.appLoader.show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -54,4 +55,5 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>((ref) => ReleaseInfoNotifier());
|
final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>(
|
||||||
|
(ref) => ReleaseInfoNotifier());
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
|||||||
: super(
|
: super(
|
||||||
ServerInfoState(
|
ServerInfoState(
|
||||||
mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
|
mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
|
||||||
serverVersion: ServerVersion(major: 0, patch: 0, minor: 0, build: 0),
|
serverVersion:
|
||||||
|
ServerVersion(major: 0, patch: 0, minor: 0, build: 0),
|
||||||
isVersionMismatch: false,
|
isVersionMismatch: false,
|
||||||
versionMismatchErrorMessage: "",
|
versionMismatchErrorMessage: "",
|
||||||
),
|
),
|
||||||
@@ -33,7 +34,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
|||||||
|
|
||||||
state = state.copyWith(serverVersion: serverVersion);
|
state = state.copyWith(serverVersion: serverVersion);
|
||||||
|
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
var packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
Map<String, int> appVersion = _getDetailVersion(packageInfo.version);
|
Map<String, int> appVersion = _getDetailVersion(packageInfo.version);
|
||||||
|
|
||||||
@@ -57,7 +58,8 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(isVersionMismatch: false, versionMismatchErrorMessage: "");
|
state = state.copyWith(
|
||||||
|
isVersionMismatch: false, versionMismatchErrorMessage: "");
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, int> _getDetailVersion(String version) {
|
Map<String, int> _getDetailVersion(String version) {
|
||||||
@@ -75,6 +77,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final serverInfoProvider = StateNotifierProvider<ServerInfoNotifier, ServerInfoState>((ref) {
|
final serverInfoProvider =
|
||||||
|
StateNotifierProvider<ServerInfoNotifier, ServerInfoState>((ref) {
|
||||||
return ServerInfoNotifier();
|
return ServerInfoNotifier();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.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/shared/providers/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:socket_io_client/socket_io_client.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/shared/models/immich_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:socket_io_client/socket_io_client.dart';
|
||||||
|
|
||||||
class WebscoketState {
|
class WebscoketState {
|
||||||
final Socket? socket;
|
final Socket? socket;
|
||||||
@@ -60,8 +59,9 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
|||||||
debugPrint("[WEBSOCKET] Attempting to connect to ws");
|
debugPrint("[WEBSOCKET] Attempting to connect to ws");
|
||||||
// Configure socket transports must be sepecified
|
// Configure socket transports must be sepecified
|
||||||
Socket socket = io(
|
Socket socket = io(
|
||||||
endpoint,
|
endpoint.toString().replaceAll('/api', ''),
|
||||||
OptionBuilder()
|
OptionBuilder()
|
||||||
|
.setPath('/api/socket.io')
|
||||||
.setTransports(['websocket'])
|
.setTransports(['websocket'])
|
||||||
.enableReconnection()
|
.enableReconnection()
|
||||||
.enableForceNew()
|
.enableForceNew()
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'dart:io' show Platform;
|
|||||||
class DeviceInfoService {
|
class DeviceInfoService {
|
||||||
Future<Map<String, dynamic>> getDeviceInfo() async {
|
Future<Map<String, dynamic>> getDeviceInfo() async {
|
||||||
// Get device info
|
// Get device info
|
||||||
String deviceId = await FlutterUdid.consistentUdid;
|
var deviceId = await FlutterUdid.consistentUdid;
|
||||||
String deviceType = "";
|
var deviceType = "";
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
deviceType = "ANDROID";
|
deviceType = "ANDROID";
|
||||||
|
|||||||
@@ -4,15 +4,22 @@ import 'dart:convert';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
||||||
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
|
|
||||||
class NetworkService {
|
class NetworkService {
|
||||||
|
late final Dio dio;
|
||||||
|
|
||||||
|
NetworkService() {
|
||||||
|
dio = Dio();
|
||||||
|
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
||||||
|
}
|
||||||
|
|
||||||
Future<dynamic> deleteRequest({required String url, dynamic data}) async {
|
Future<dynamic> deleteRequest({required String url, dynamic data}) async {
|
||||||
try {
|
try {
|
||||||
var dio = Dio();
|
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
|
||||||
|
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
Response res = await dio.delete('$savedEndpoint/$url', data: data);
|
Response res = await dio.delete('$savedEndpoint/$url', data: data);
|
||||||
|
|
||||||
@@ -26,11 +33,11 @@ class NetworkService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> getRequest({required String url, bool isByteResponse = false, bool isStreamReponse = false}) async {
|
Future<dynamic> getRequest(
|
||||||
|
{required String url,
|
||||||
|
bool isByteResponse = false,
|
||||||
|
bool isStreamReponse = false}) async {
|
||||||
try {
|
try {
|
||||||
var dio = Dio();
|
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
|
||||||
|
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
|
|
||||||
if (isByteResponse) {
|
if (isByteResponse) {
|
||||||
@@ -66,12 +73,9 @@ class NetworkService {
|
|||||||
|
|
||||||
Future<dynamic> postRequest({required String url, dynamic data}) async {
|
Future<dynamic> postRequest({required String url, dynamic data}) async {
|
||||||
try {
|
try {
|
||||||
var dio = Dio();
|
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
|
||||||
|
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
String validUrl = Uri.parse('$savedEndpoint/$url').toString();
|
var validUrl = Uri.parse('$savedEndpoint/$url').toString();
|
||||||
Response res = await dio.post(validUrl, data: data);
|
var res = await dio.post(validUrl, data: data);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
} on DioError catch (e) {
|
} on DioError catch (e) {
|
||||||
@@ -85,12 +89,9 @@ class NetworkService {
|
|||||||
|
|
||||||
Future<dynamic> putRequest({required String url, dynamic data}) async {
|
Future<dynamic> putRequest({required String url, dynamic data}) async {
|
||||||
try {
|
try {
|
||||||
var dio = Dio();
|
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
|
||||||
|
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
String validUrl = Uri.parse('$savedEndpoint/$url').toString();
|
var validUrl = Uri.parse('$savedEndpoint/$url').toString();
|
||||||
Response res = await dio.put(validUrl, data: data);
|
var res = await dio.put(validUrl, data: data);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
} on DioError catch (e) {
|
} on DioError catch (e) {
|
||||||
@@ -104,13 +105,9 @@ class NetworkService {
|
|||||||
|
|
||||||
Future<dynamic> patchRequest({required String url, dynamic data}) async {
|
Future<dynamic> patchRequest({required String url, dynamic data}) async {
|
||||||
try {
|
try {
|
||||||
var dio = Dio();
|
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
|
||||||
|
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
|
var validUrl = Uri.parse('$savedEndpoint/$url').toString();
|
||||||
String validUrl = Uri.parse('$savedEndpoint/$url').toString();
|
var res = await dio.patch(validUrl, data: data);
|
||||||
Response res = await dio.patch(validUrl, data: data);
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
} on DioError catch (e) {
|
} on DioError catch (e) {
|
||||||
@@ -122,21 +119,15 @@ class NetworkService {
|
|||||||
|
|
||||||
Future<bool> pingServer() async {
|
Future<bool> pingServer() async {
|
||||||
try {
|
try {
|
||||||
var dio = Dio();
|
|
||||||
|
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
|
var validUrl = Uri.parse('$savedEndpoint/server-info/ping').toString();
|
||||||
|
|
||||||
String validUrl = Uri.parse('$savedEndpoint/server-info/ping').toString();
|
debugPrint("ping server at url $validUrl");
|
||||||
|
|
||||||
debugPrint("pint server at url $validUrl");
|
var res = await dio.get(validUrl);
|
||||||
Response res = await dio.get(validUrl);
|
|
||||||
var jsonRespsonse = jsonDecode(res.toString());
|
var jsonRespsonse = jsonDecode(res.toString());
|
||||||
|
|
||||||
if (jsonRespsonse["res"] == "pong") {
|
return jsonRespsonse["res"] == "pong";
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} on DioError catch (e) {
|
} on DioError catch (e) {
|
||||||
debugPrint("[PING SERVER] DioError: ${e.response} - $e");
|
debugPrint("[PING SERVER] DioError: ${e.response} - $e");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
|
||||||
|
|
||||||
class ServerInfoService {
|
class ServerInfoService {
|
||||||
final NetworkService _networkService = NetworkService();
|
final NetworkService _networkService = NetworkService();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/shared/models/upload_profile_image_repsonse.model.dart';
|
import 'package:immich_mobile/shared/models/upload_profile_image_repsonse.model.dart';
|
||||||
@@ -10,14 +11,13 @@ import 'package:immich_mobile/shared/models/user.model.dart';
|
|||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
||||||
import 'package:immich_mobile/utils/files_helper.dart';
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
final NetworkService _networkService = NetworkService();
|
final NetworkService _networkService = NetworkService();
|
||||||
|
|
||||||
Future<List<User>> getAllUsersInfo() async {
|
Future<List<User>> getAllUsersInfo() async {
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.getRequest(url: 'user');
|
var res = await _networkService.getRequest(url: 'user');
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||||
List<User> result = List.from(decodedData.map((e) => User.fromMap(e)));
|
List<User> result = List.from(decodedData.map((e) => User.fromMap(e)));
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ class ImmichToast {
|
|||||||
ToastType toastType = ToastType.info,
|
ToastType toastType = ToastType.info,
|
||||||
ToastGravity gravity = ToastGravity.TOP,
|
ToastGravity gravity = ToastGravity.TOP,
|
||||||
}) {
|
}) {
|
||||||
FToast fToast;
|
final fToast = FToast();
|
||||||
|
|
||||||
fToast = FToast();
|
|
||||||
fToast.init(context);
|
fToast.init(context);
|
||||||
|
|
||||||
_getColor(ToastType type, BuildContext context) {
|
Color _getColor(ToastType type, BuildContext context) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ToastType.info:
|
case ToastType.info:
|
||||||
return Theme.of(context).primaryColor;
|
return Theme.of(context).primaryColor;
|
||||||
@@ -26,6 +24,26 @@ class ImmichToast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Icon _getIcon(ToastType type) {
|
||||||
|
switch (type) {
|
||||||
|
case ToastType.info:
|
||||||
|
return Icon(
|
||||||
|
Icons.info_outline_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
case ToastType.success:
|
||||||
|
return const Icon(
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
color: Color.fromARGB(255, 78, 140, 124),
|
||||||
|
);
|
||||||
|
case ToastType.error:
|
||||||
|
return const Icon(
|
||||||
|
Icons.error_outline_rounded,
|
||||||
|
color: Color.fromARGB(255, 240, 162, 156),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fToast.showToast(
|
fToast.showToast(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
|
||||||
@@ -40,24 +58,7 @@ class ImmichToast {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
(toastType == ToastType.info)
|
_getIcon(toastType),
|
||||||
? Icon(
|
|
||||||
Icons.info_outline_rounded,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
(toastType == ToastType.success)
|
|
||||||
? const Icon(
|
|
||||||
Icons.check_circle_rounded,
|
|
||||||
color: Color.fromARGB(255, 78, 140, 124),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
(toastType == ToastType.error)
|
|
||||||
? const Icon(
|
|
||||||
Icons.error_outline_rounded,
|
|
||||||
color: Color.fromARGB(255, 240, 162, 156),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 12.0,
|
width: 12.0,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,25 +9,25 @@ class ImmichLoadingOverlay extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder<bool>(
|
return ValueListenableBuilder<bool>(
|
||||||
valueListenable: ImmichLoadingOverlayController.appLoader.loaderShowingNotifier,
|
valueListenable:
|
||||||
|
ImmichLoadingOverlayController.appLoader.loaderShowingNotifier,
|
||||||
builder: (context, shouldShow, child) {
|
builder: (context, shouldShow, child) {
|
||||||
if (shouldShow) {
|
return shouldShow
|
||||||
return const Scaffold(
|
? const Scaffold(
|
||||||
backgroundColor: Colors.black54,
|
backgroundColor: Colors.black54,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: ImmichLoadingIndicator(),
|
child: ImmichLoadingIndicator(),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
} else {
|
: const SizedBox();
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImmichLoadingOverlayController {
|
class ImmichLoadingOverlayController {
|
||||||
static final ImmichLoadingOverlayController appLoader = ImmichLoadingOverlayController();
|
static final ImmichLoadingOverlayController appLoader =
|
||||||
|
ImmichLoadingOverlayController();
|
||||||
ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false);
|
ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false);
|
||||||
ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
|
ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
|
||||||
|
|
||||||
|
|||||||
72
mobile/lib/shared/views/splash_screen.dart
Normal file
72
mobile/lib/shared/views/splash_screen.dart
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.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/constants/immich_colors.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
class SplashScreenPage extends HookConsumerWidget {
|
||||||
|
const SplashScreenPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
HiveSavedLoginInfo? loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
|
||||||
|
|
||||||
|
void performLoggingIn() async {
|
||||||
|
var isAuthenticated = await ref
|
||||||
|
.read(authenticationProvider.notifier)
|
||||||
|
.login(loginInfo!.email, loginInfo.password, loginInfo.serverUrl, true);
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
// Resume backup (if enable) then navigate
|
||||||
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
|
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||||
|
} else {
|
||||||
|
AutoRouter.of(context).push(const LoginRoute());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (loginInfo != null && loginInfo.isSaveLogin) {
|
||||||
|
performLoggingIn();
|
||||||
|
} else {
|
||||||
|
AutoRouter.of(context).push(const LoginRoute());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: immichBackgroundColor,
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Image(
|
||||||
|
image: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
width: 200,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
'IMMICH',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SnowburstOne',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 48,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,9 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
void goToReleaseNote() async {
|
void goToReleaseNote() async {
|
||||||
final Uri _url = Uri.parse('https://github.com/alextran1502/immich/releases/latest');
|
final Uri url =
|
||||||
await launchUrl(_url);
|
Uri.parse('https://github.com/alextran1502/immich/releases/latest');
|
||||||
|
await launchUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAcknowledgeTapped() {
|
void onAcknowledgeTapped() {
|
||||||
@@ -21,7 +22,8 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ValueListenableBuilder<bool>(
|
return ValueListenableBuilder<bool>(
|
||||||
valueListenable: VersionAnnouncementOverlayController.appLoader.loaderShowingNotifier,
|
valueListenable:
|
||||||
|
VersionAnnouncementOverlayController.appLoader.loaderShowingNotifier,
|
||||||
builder: (context, shouldShow, child) {
|
builder: (context, shouldShow, child) {
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -51,10 +53,14 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
|
|||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14, fontFamily: 'WorkSans', color: Colors.black87, height: 1.2),
|
fontSize: 14,
|
||||||
|
fontFamily: 'WorkSans',
|
||||||
|
color: Colors.black87,
|
||||||
|
height: 1.2),
|
||||||
children: <TextSpan>[
|
children: <TextSpan>[
|
||||||
const TextSpan(
|
const TextSpan(
|
||||||
text: 'Hi friend, there is a new release of',
|
text:
|
||||||
|
'Hi friend, there is a new release of',
|
||||||
),
|
),
|
||||||
const TextSpan(
|
const TextSpan(
|
||||||
text: ' Immich ',
|
text: ' Immich ',
|
||||||
@@ -65,19 +71,21 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(
|
const TextSpan(
|
||||||
text: "please take your time to visit the ",
|
text:
|
||||||
|
"please take your time to visit the ",
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: "release note",
|
text: "release note",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
recognizer: TapGestureRecognizer()..onTap = goToReleaseNote,
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = goToReleaseNote,
|
||||||
),
|
),
|
||||||
const TextSpan(
|
const TextSpan(
|
||||||
text:
|
text:
|
||||||
" 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.",
|
" 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.",
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -85,22 +93,24 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
primary: Colors.indigo,
|
primary: Colors.indigo,
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10, horizontal: 25),
|
||||||
|
),
|
||||||
|
onPressed: onAcknowledgeTapped,
|
||||||
|
child: const Text(
|
||||||
|
"Acknowledge",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
onPressed: onAcknowledgeTapped,
|
),
|
||||||
child: const Text(
|
),
|
||||||
"Acknowledge",
|
),
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -119,7 +129,8 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class VersionAnnouncementOverlayController {
|
class VersionAnnouncementOverlayController {
|
||||||
static final VersionAnnouncementOverlayController appLoader = VersionAnnouncementOverlayController();
|
static final VersionAnnouncementOverlayController appLoader =
|
||||||
|
VersionAnnouncementOverlayController();
|
||||||
ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false);
|
ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false);
|
||||||
ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
|
ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
@@ -233,7 +233,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.1"
|
version: "0.17.1"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cupertino_icons
|
name: cupertino_icons
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
@@ -334,7 +334,7 @@ packages:
|
|||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "2.0.1"
|
||||||
flutter_map:
|
flutter_map:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -465,12 +465,12 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.1"
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -542,7 +542,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.0"
|
version: "4.5.0"
|
||||||
latlong2:
|
latlong2:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: latlong2
|
name: latlong2
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
@@ -554,7 +554,7 @@ packages:
|
|||||||
name: lints
|
name: lints
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "2.0.0"
|
||||||
lists:
|
lists:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -668,19 +668,19 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.1"
|
version: "1.8.1"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.10"
|
version: "2.0.11"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -757,7 +757,7 @@ packages:
|
|||||||
name: photo_view
|
name: photo_view
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.14.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -861,13 +861,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.99"
|
||||||
sliver_tools:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: sliver_tools
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.6"
|
|
||||||
socket_io_client:
|
socket_io_client:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1141,13 +1134,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.10"
|
version: "2.0.10"
|
||||||
visibility_detector:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: visibility_detector
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.2"
|
|
||||||
wakelock:
|
wakelock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.12.0+18
|
version: 1.13.0+20
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.15.1 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.2
|
|
||||||
photo_manager: ^2.0.6
|
photo_manager: ^2.0.6
|
||||||
flutter_hooks: ^0.18.0
|
flutter_hooks: ^0.18.0
|
||||||
hooks_riverpod: ^2.0.0-dev.0
|
hooks_riverpod: ^2.0.0-dev.0
|
||||||
@@ -23,14 +23,12 @@ dependencies:
|
|||||||
auto_route: ^4.0.1
|
auto_route: ^4.0.1
|
||||||
exif: ^3.1.1
|
exif: ^3.1.1
|
||||||
transparent_image: ^2.0.0
|
transparent_image: ^2.0.0
|
||||||
visibility_detector: ^0.2.2
|
|
||||||
flutter_launcher_icons: "^0.9.2"
|
flutter_launcher_icons: "^0.9.2"
|
||||||
fluttertoast: ^8.0.8
|
fluttertoast: ^8.0.8
|
||||||
video_player: ^2.2.18
|
video_player: ^2.2.18
|
||||||
chewie: ^1.2.2
|
chewie: ^1.2.2
|
||||||
sliver_tools: ^0.2.5
|
|
||||||
badges: ^2.0.2
|
badges: ^2.0.2
|
||||||
photo_view: ^0.13.0
|
photo_view: ^0.14.0
|
||||||
socket_io_client: ^2.0.0-beta.4-nullsafety.0
|
socket_io_client: ^2.0.0-beta.4-nullsafety.0
|
||||||
flutter_map: ^0.14.0
|
flutter_map: ^0.14.0
|
||||||
flutter_udid: ^2.0.0
|
flutter_udid: ^2.0.0
|
||||||
@@ -43,10 +41,16 @@ dependencies:
|
|||||||
http: 0.13.4
|
http: 0.13.4
|
||||||
cancellation_token_http: ^1.1.0
|
cancellation_token_http: ^1.1.0
|
||||||
|
|
||||||
|
path: ^1.8.1
|
||||||
|
path_provider: ^2.0.11
|
||||||
|
latlong2: ^0.8.1
|
||||||
|
collection: ^1.16.0
|
||||||
|
http_parser: ^4.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^1.0.0
|
flutter_lints: ^2.0.1
|
||||||
hive_generator: ^1.1.2
|
hive_generator: ^1.1.2
|
||||||
build_runner: ^2.1.7
|
build_runner: ^2.1.7
|
||||||
auto_route_generator: ^4.0.0
|
auto_route_generator: ^4.0.0
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Logger,
|
Logger,
|
||||||
Patch,
|
Patch,
|
||||||
|
HttpCode,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
@@ -76,6 +77,10 @@ export class AssetController {
|
|||||||
{ asset: assetWithThumbnail, fileName: file.originalname, fileSize: file.size, hasThumbnail: true },
|
{ asset: assetWithThumbnail, fileName: file.originalname, fileSize: file.size, hasThumbnail: true },
|
||||||
{ jobId: savedAsset.id },
|
{ jobId: savedAsset.id },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.wsCommunicateionGateway.server
|
||||||
|
.to(savedAsset.userId)
|
||||||
|
.emit('on_upload_success', JSON.stringify(assetWithThumbnail));
|
||||||
} else {
|
} else {
|
||||||
await this.assetUploadedQueue.add(
|
await this.assetUploadedQueue.add(
|
||||||
'asset-uploaded',
|
'asset-uploaded',
|
||||||
@@ -83,8 +88,6 @@ export class AssetController {
|
|||||||
{ jobId: savedAsset.id },
|
{ jobId: savedAsset.id },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(`Error receiving upload file ${e}`);
|
Logger.error(`Error receiving upload file ${e}`);
|
||||||
}
|
}
|
||||||
@@ -171,4 +174,20 @@ export class AssetController {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check duplicated asset before uploading - for Web upload used
|
||||||
|
*/
|
||||||
|
@Post('/check')
|
||||||
|
@HttpCode(200)
|
||||||
|
async checkDuplicateAsset(
|
||||||
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
|
@Body(ValidationPipe) { deviceAssetId }: { deviceAssetId: string },
|
||||||
|
) {
|
||||||
|
const res = await this.assetService.checkDuplicatedAsset(authUser, deviceAssetId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isExist: res,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ import { CommunicationModule } from '../communication/communication.module';
|
|||||||
],
|
],
|
||||||
controllers: [AssetController],
|
controllers: [AssetController],
|
||||||
providers: [AssetService, BackgroundTaskService],
|
providers: [AssetService, BackgroundTaskService],
|
||||||
exports: [],
|
exports: [AssetService],
|
||||||
})
|
})
|
||||||
export class AssetModule {}
|
export class AssetModule {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
|
import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { IsNull, Not, Repository } from 'typeorm';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
||||||
@@ -72,6 +72,7 @@ export class AssetService {
|
|||||||
return await this.assetRepository.find({
|
return await this.assetRepository.find({
|
||||||
where: {
|
where: {
|
||||||
userId: authUser.id,
|
userId: authUser.id,
|
||||||
|
resizePath: Not(IsNull()),
|
||||||
},
|
},
|
||||||
relations: ['exifInfo'],
|
relations: ['exifInfo'],
|
||||||
order: {
|
order: {
|
||||||
@@ -243,7 +244,7 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Sending Partial Content With HTTP Code 206 */
|
/** Sending Partial Content With HTTP Code 206 */
|
||||||
|
console.log('Send Range', range);
|
||||||
res.status(206).set({
|
res.status(206).set({
|
||||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||||
'Accept-Ranges': 'bytes',
|
'Accept-Ranges': 'bytes',
|
||||||
@@ -381,4 +382,15 @@ export class AssetService {
|
|||||||
[authUser.id],
|
[authUser.id],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkDuplicatedAsset(authUser: AuthUserDto, deviceAssetId: string) {
|
||||||
|
const res = await this.assetRepository.findOne({
|
||||||
|
where: {
|
||||||
|
deviceAssetId,
|
||||||
|
userId: authUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res ? true : false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { Logger } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { UserEntity } from '@app/database/entities/user.entity';
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
import { query } from 'express';
|
||||||
|
|
||||||
@WebSocketGateway()
|
@WebSocketGateway({ cors: true })
|
||||||
export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
constructor(
|
constructor(
|
||||||
private immichJwtService: ImmichJwtService,
|
private immichJwtService: ImmichJwtService,
|
||||||
@@ -21,27 +22,33 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
|
|||||||
handleDisconnect(client: Socket) {
|
handleDisconnect(client: Socket) {
|
||||||
client.leave(client.nsp.name);
|
client.leave(client.nsp.name);
|
||||||
|
|
||||||
Logger.log(`Client ${client.id} disconnected`);
|
Logger.log(`Client ${client.id} disconnected from Websocket`, 'WebsocketConnectionEvent');
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleConnection(client: Socket, ...args: any[]) {
|
async handleConnection(client: Socket, ...args: any[]) {
|
||||||
Logger.log(`New websocket connection: ${client.id}`, 'NewWebSocketConnection');
|
try {
|
||||||
const accessToken = client.handshake.headers.authorization.split(' ')[1];
|
Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent');
|
||||||
const res = await this.immichJwtService.validateToken(accessToken);
|
|
||||||
|
|
||||||
if (!res.status) {
|
const accessToken = client.handshake.headers.authorization.split(' ')[1];
|
||||||
client.emit('error', 'unauthorized');
|
|
||||||
client.disconnect();
|
const res = await this.immichJwtService.validateToken(accessToken);
|
||||||
return;
|
|
||||||
|
if (!res.status) {
|
||||||
|
client.emit('error', 'unauthorized');
|
||||||
|
client.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userRepository.findOne({ where: { id: res.userId } });
|
||||||
|
if (!user) {
|
||||||
|
client.emit('error', 'unauthorized');
|
||||||
|
client.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.join(user.id);
|
||||||
|
} catch (e) {
|
||||||
|
// Logger.error(`Error establish websocket conneciton ${e}`, 'HandleWebscoketConnection');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.userRepository.findOne({ where: { id: res.userId } });
|
|
||||||
if (!user) {
|
|
||||||
client.emit('error', 'unauthorized');
|
|
||||||
client.disconnect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
client.join(user.id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const immichAppConfig: ConfigModuleOptions = {
|
|||||||
DB_USERNAME: Joi.string().required(),
|
DB_USERNAME: Joi.string().required(),
|
||||||
DB_PASSWORD: Joi.string().required(),
|
DB_PASSWORD: Joi.string().required(),
|
||||||
DB_DATABASE_NAME: Joi.string().required(),
|
DB_DATABASE_NAME: Joi.string().required(),
|
||||||
UPLOAD_LOCATION: Joi.string().required(),
|
|
||||||
JWT_SECRET: Joi.string().required(),
|
JWT_SECRET: Joi.string().required(),
|
||||||
ENABLE_MAPBOX: Joi.boolean().required().valid(true, false),
|
ENABLE_MAPBOX: Joi.boolean().required().valid(true, false),
|
||||||
MAPBOX_KEY: Joi.any().when('ENABLE_MAPBOX', {
|
MAPBOX_KEY: Joi.any().when('ENABLE_MAPBOX', {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
export const serverVersion = {
|
export const serverVersion = {
|
||||||
major: 1,
|
major: 1,
|
||||||
minor: 12,
|
minor: 13,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
build: 18,
|
build: 20,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
|
|
||||||
app.enableCors();
|
|
||||||
|
|
||||||
app.set('trust proxy');
|
app.set('trust proxy');
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
app.enableCors();
|
||||||
|
}
|
||||||
|
|
||||||
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||||
|
|
||||||
await app.listen(3000, () => {
|
await app.listen(3000, () => {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { RedisIoAdapter } from '../../immich/src/middlewares/redis-io.adapter.middleware';
|
||||||
import { MicroservicesModule } from './microservices.module';
|
import { MicroservicesModule } from './microservices.module';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(MicroservicesModule);
|
const app = await NestFactory.create(MicroservicesModule);
|
||||||
|
|
||||||
|
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||||
|
|
||||||
await app.listen(3000, () => {
|
await app.listen(3000, () => {
|
||||||
if (process.env.NODE_ENV == 'development') {
|
if (process.env.NODE_ENV == 'development') {
|
||||||
Logger.log('Running Immich Microservices in DEVELOPMENT environment', 'ImmichMicroservice');
|
Logger.log('Running Immich Microservices in DEVELOPMENT environment', 'ImmichMicroservice');
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { AssetUploadedProcessor } from './processors/asset-uploaded.processor';
|
|||||||
import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor';
|
import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor';
|
||||||
import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
|
import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
|
||||||
import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
|
import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
|
||||||
|
import { AssetModule } from '../../immich/src/api-v1/asset/asset.module';
|
||||||
|
import { CommunicationGateway } from '../../immich/src/api-v1/communication/communication.gateway';
|
||||||
|
import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -56,6 +59,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor'
|
|||||||
removeOnFail: false,
|
removeOnFail: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
CommunicationModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export class AssetUploadedProcessor {
|
|||||||
await this.metadataExtractionQueue.add('detect-object', { asset }, { jobId: randomUUID() });
|
await this.metadataExtractionQueue.add('detect-object', { asset }, { jobId: randomUUID() });
|
||||||
} else {
|
} else {
|
||||||
// Generate Thumbnail -> Then generate webp, tag image and detect object
|
// Generate Thumbnail -> Then generate webp, tag image and detect object
|
||||||
|
await this.thumbnailGeneratorQueue.add('generate-jpeg-thumbnail', { asset }, { jobId: randomUUID() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video Conversion
|
// Video Conversion
|
||||||
@@ -63,5 +64,10 @@ export class AssetUploadedProcessor {
|
|||||||
{ jobId: randomUUID() },
|
{ jobId: randomUUID() },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract video duration if uploaded from the web
|
||||||
|
if (asset.type == AssetType.VIDEO && asset.duration == '0:00:00.000000') {
|
||||||
|
await this.metadataExtractionQueue.add('extract-video-length', { asset }, { jobId: randomUUID() });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { Logger } from '@nestjs/common';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import ffmpeg from 'fluent-ffmpeg';
|
||||||
|
// import moment from 'moment';
|
||||||
|
|
||||||
@Processor('metadata-extraction-queue')
|
@Processor('metadata-extraction-queue')
|
||||||
export class MetadataExtractionProcessor {
|
export class MetadataExtractionProcessor {
|
||||||
@@ -129,4 +131,27 @@ export class MetadataExtractionProcessor {
|
|||||||
Logger.error(`Failed to trigger object detection pipe line ${error.toString()}`);
|
Logger.error(`Failed to trigger object detection pipe line ${error.toString()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Process({ name: 'extract-video-length', concurrency: 2 })
|
||||||
|
async extractVideoLength(job: Job) {
|
||||||
|
const { asset }: { asset: AssetEntity } = job.data;
|
||||||
|
|
||||||
|
ffmpeg.ffprobe(asset.originalPath, async (err, data) => {
|
||||||
|
if (!err) {
|
||||||
|
if (data.format.duration) {
|
||||||
|
const videoDurationInSecond = parseInt(data.format.duration.toString(), 0);
|
||||||
|
|
||||||
|
const hours = Math.floor(videoDurationInSecond / 3600);
|
||||||
|
const minutes = Math.floor((videoDurationInSecond - hours * 3600) / 60);
|
||||||
|
const seconds = videoDurationInSecond - hours * 3600 - minutes * 60;
|
||||||
|
|
||||||
|
const durationString = `${hours}:${minutes < 10 ? '0' + minutes.toString() : minutes}:${
|
||||||
|
seconds < 10 ? '0' + seconds.toString() : seconds
|
||||||
|
}.000000`;
|
||||||
|
|
||||||
|
await this.assetRepository.update({ id: asset.id }, { duration: durationString });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,89 @@
|
|||||||
import { Process, Processor } from '@nestjs/bull';
|
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
||||||
import { Job } from 'bull';
|
import { Job, Queue } from 'bull';
|
||||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
||||||
import { Repository } from 'typeorm/repository/Repository';
|
import { Repository } from 'typeorm/repository/Repository';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
import { existsSync, mkdirSync } from 'node:fs';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { CommunicationGateway } from '../../../immich/src/api-v1/communication/communication.gateway';
|
||||||
|
import ffmpeg from 'fluent-ffmpeg';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
@Processor('thumbnail-generator-queue')
|
@Processor('thumbnail-generator-queue')
|
||||||
export class ThumbnailGeneratorProcessor {
|
export class ThumbnailGeneratorProcessor {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
|
|
||||||
|
@InjectQueue('thumbnail-generator-queue')
|
||||||
|
private thumbnailGeneratorQueue: Queue,
|
||||||
|
|
||||||
|
private wsCommunicateionGateway: CommunicationGateway,
|
||||||
|
|
||||||
|
@InjectQueue('metadata-extraction-queue')
|
||||||
|
private metadataExtractionQueue: Queue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process('generate-jpeg-thumbnail')
|
@Process('generate-jpeg-thumbnail')
|
||||||
async generateJPEGThumbnail(job: Job) {
|
async generateJPEGThumbnail(job: Job) {
|
||||||
const { asset }: { asset: AssetEntity } = job.data;
|
const { asset }: { asset: AssetEntity } = job.data;
|
||||||
|
|
||||||
console.log(asset);
|
const resizePath = `upload/${asset.userId}/thumb/${asset.deviceId}/`;
|
||||||
|
|
||||||
|
if (!existsSync(resizePath)) {
|
||||||
|
mkdirSync(resizePath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const temp = asset.originalPath.split('/');
|
||||||
|
const originalFilename = temp[temp.length - 1].split('.')[0];
|
||||||
|
const jpegThumbnailPath = resizePath + originalFilename + '.jpeg';
|
||||||
|
|
||||||
|
if (asset.type == AssetType.IMAGE) {
|
||||||
|
sharp(asset.originalPath)
|
||||||
|
.resize(1440, 2560, { fit: 'inside' })
|
||||||
|
.jpeg()
|
||||||
|
.toFile(jpegThumbnailPath, async (err, info) => {
|
||||||
|
if (!err) {
|
||||||
|
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||||
|
|
||||||
|
// Update resize path to send to generate webp queue
|
||||||
|
asset.resizePath = jpegThumbnailPath;
|
||||||
|
|
||||||
|
await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset }, { jobId: randomUUID() });
|
||||||
|
await this.metadataExtractionQueue.add('tag-image', { asset }, { jobId: randomUUID() });
|
||||||
|
await this.metadataExtractionQueue.add('detect-object', { asset }, { jobId: randomUUID() });
|
||||||
|
this.wsCommunicateionGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(asset));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.type == AssetType.VIDEO) {
|
||||||
|
ffmpeg(asset.originalPath)
|
||||||
|
.outputOptions(['-ss 00:00:01.000', '-frames:v 1'])
|
||||||
|
.output(jpegThumbnailPath)
|
||||||
|
.on('start', () => {
|
||||||
|
Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail');
|
||||||
|
})
|
||||||
|
.on('error', (error, b, c) => {
|
||||||
|
Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail');
|
||||||
|
// reject();
|
||||||
|
})
|
||||||
|
.on('end', async () => {
|
||||||
|
Logger.log(`Generating Video Thumbnail Success ${asset.id}`, 'generateJPEGThumbnail');
|
||||||
|
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||||
|
|
||||||
|
// Update resize path to send to generate webp queue
|
||||||
|
asset.resizePath = jpegThumbnailPath;
|
||||||
|
|
||||||
|
await this.thumbnailGeneratorQueue.add('generate-webp-thumbnail', { asset }, { jobId: randomUUID() });
|
||||||
|
await this.metadataExtractionQueue.add('tag-image', { asset }, { jobId: randomUUID() });
|
||||||
|
await this.metadataExtractionQueue.add('detect-object', { asset }, { jobId: randomUUID() });
|
||||||
|
|
||||||
|
this.wsCommunicateionGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(asset));
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Process({ name: 'generate-webp-thumbnail', concurrency: 2 })
|
@Process({ name: 'generate-webp-thumbnail', concurrency: 2 })
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class VideoTranscodeProcessor {
|
|||||||
.outputOptions(['-crf 23', '-preset ultrafast', '-vcodec libx264', '-acodec mp3', '-vf scale=1280:-2'])
|
.outputOptions(['-crf 23', '-preset ultrafast', '-vcodec libx264', '-acodec mp3', '-vf scale=1280:-2'])
|
||||||
.output(savedEncodedPath)
|
.output(savedEncodedPath)
|
||||||
.on('start', () => {
|
.on('start', () => {
|
||||||
Logger.log('Start Converting', 'mp4Conversion');
|
Logger.log('Start Converting Video', 'mp4Conversion');
|
||||||
})
|
})
|
||||||
.on('error', (error, b, c) => {
|
.on('error', (error, b, c) => {
|
||||||
Logger.error(`Cannot Convert Video ${error}`, 'mp4Conversion');
|
Logger.error(`Cannot Convert Video ${error}`, 'mp4Conversion');
|
||||||
|
|||||||
19
server/package-lock.json
generated
19
server/package-lock.json
generated
@@ -55,6 +55,7 @@
|
|||||||
"@types/bull": "^3.15.7",
|
"@types/bull": "^3.15.7",
|
||||||
"@types/cron": "^2.0.0",
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/fluent-ffmpeg": "^2.1.20",
|
||||||
"@types/imagemin": "^8.0.0",
|
"@types/imagemin": "^8.0.0",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
@@ -2195,6 +2196,15 @@
|
|||||||
"@types/range-parser": "*"
|
"@types/range-parser": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/fluent-ffmpeg": {
|
||||||
|
"version": "2.1.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.20.tgz",
|
||||||
|
"integrity": "sha512-B+OvhCdJ3LgEq2PhvWNOiB/EfwnXLElfMCgc4Z1K5zXgSfo9I6uGKwR/lqmNPFQuebNnes7re3gqkV77SyypLg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||||
@@ -12803,6 +12813,15 @@
|
|||||||
"@types/range-parser": "*"
|
"@types/range-parser": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/fluent-ffmpeg": {
|
||||||
|
"version": "2.1.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.20.tgz",
|
||||||
|
"integrity": "sha512-B+OvhCdJ3LgEq2PhvWNOiB/EfwnXLElfMCgc4Z1K5zXgSfo9I6uGKwR/lqmNPFQuebNnes7re3gqkV77SyypLg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/graceful-fs": {
|
"@types/graceful-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"@types/bull": "^3.15.7",
|
"@types/bull": "^3.15.7",
|
||||||
"@types/cron": "^2.0.0",
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/fluent-ffmpeg": "^2.1.20",
|
||||||
"@types/imagemin": "^8.0.0",
|
"@types/imagemin": "^8.0.0",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
|
|||||||
295
web/package-lock.json
generated
295
web/package-lock.json
generated
@@ -10,11 +10,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"cookie": "^0.4.2",
|
"cookie": "^0.4.2",
|
||||||
|
"exifr": "^7.1.3",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"markdown-it": "^13.0.1",
|
|
||||||
"moment": "^2.29.3",
|
"moment": "^2.29.3",
|
||||||
|
"socket.io-client": "^4.5.1",
|
||||||
"svelte-material-icons": "^2.0.2"
|
"svelte-material-icons": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
"@types/leaflet": "^1.7.10",
|
"@types/leaflet": "^1.7.10",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/markdown-it": "^12.2.3",
|
"@types/socket.io-client": "^3.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||||
"@typescript-eslint/parser": "^5.10.1",
|
"@typescript-eslint/parser": "^5.10.1",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
@@ -140,6 +141,11 @@
|
|||||||
"node": ">= 8.0.0"
|
"node": ">= 8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||||
|
},
|
||||||
"node_modules/@sveltejs/adapter-auto": {
|
"node_modules/@sveltejs/adapter-auto": {
|
||||||
"version": "1.0.0-next.40",
|
"version": "1.0.0-next.40",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-1.0.0-next.40.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-1.0.0-next.40.tgz",
|
||||||
@@ -293,12 +299,6 @@
|
|||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/linkify-it": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.14.182",
|
"version": "4.14.182",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
||||||
@@ -314,22 +314,6 @@
|
|||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/markdown-it": {
|
|
||||||
"version": "12.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
|
||||||
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/linkify-it": "*",
|
|
||||||
"@types/mdurl": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/mdurl": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "17.0.32",
|
"version": "17.0.32",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz",
|
||||||
@@ -351,6 +335,16 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/socket.io-client": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-s+IPvFoEIjKA3RdJz/Z2dGR4gLgysKi8owcnrVwNjgvc01Lk68LJDDsG2GRqegFITcxmvCMYM7bhMpwEMlHmDg==",
|
||||||
|
"deprecated": "This is a stub types definition. socket.io-client provides its own type definitions, so you do not need this installed.",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"socket.io-client": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.23.0",
|
"version": "5.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz",
|
||||||
@@ -650,7 +644,8 @@
|
|||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/array-union": {
|
"node_modules/array-union": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
@@ -933,7 +928,6 @@
|
|||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
@@ -1043,15 +1037,24 @@
|
|||||||
"integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==",
|
"integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/engine.io-client": {
|
||||||
"version": "3.0.1",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
|
||||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
|
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.0.3",
|
||||||
|
"ws": "~8.2.3",
|
||||||
|
"xmlhttprequest-ssl": "~2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=10.0.0"
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es6-promise": {
|
"node_modules/es6-promise": {
|
||||||
@@ -1673,6 +1676,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/exifr": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -2112,14 +2120,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/linkify-it": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
|
||||||
"dependencies": {
|
|
||||||
"uc.micro": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
@@ -2160,26 +2160,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/markdown-it": {
|
|
||||||
"version": "13.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
|
||||||
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"argparse": "^2.0.1",
|
|
||||||
"entities": "~3.0.1",
|
|
||||||
"linkify-it": "^4.0.1",
|
|
||||||
"mdurl": "^1.0.1",
|
|
||||||
"uc.micro": "^1.0.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"markdown-it": "bin/markdown-it.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdurl": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
|
|
||||||
},
|
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -2289,8 +2269,7 @@
|
|||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.4",
|
"version": "3.3.4",
|
||||||
@@ -2820,6 +2799,32 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-e6nLVgiRYatS+AHXnOnGi4ocOpubvOUCGhyWw8v+/FxW8saHkinG6Dfhi9TU0Kt/8mwJIAASxvw6eujQmjdZVA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.2.1",
|
||||||
|
"socket.io-parser": "~4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-tLfmEwcEwnlQTxFB7jibL/q2+q8dlVQzj4JdRLJ/W/G1+Fu9VSxCx1Lo+n1HvXxKnM//dUuD0xgiA7tQf57Vng==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sorcery": {
|
"node_modules/sorcery": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
|
||||||
@@ -3187,11 +3192,6 @@
|
|||||||
"node": ">=4.2.0"
|
"node": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uc.micro": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
|
||||||
},
|
|
||||||
"node_modules/uri-js": {
|
"node_modules/uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
@@ -3293,6 +3293,34 @@
|
|||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||||
|
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
@@ -3395,6 +3423,11 @@
|
|||||||
"picomatch": "^2.2.2"
|
"picomatch": "^2.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||||
|
},
|
||||||
"@sveltejs/adapter-auto": {
|
"@sveltejs/adapter-auto": {
|
||||||
"version": "1.0.0-next.40",
|
"version": "1.0.0-next.40",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-1.0.0-next.40.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-1.0.0-next.40.tgz",
|
||||||
@@ -3525,12 +3558,6 @@
|
|||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/linkify-it": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/lodash": {
|
"@types/lodash": {
|
||||||
"version": "4.14.182",
|
"version": "4.14.182",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
||||||
@@ -3546,22 +3573,6 @@
|
|||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/markdown-it": {
|
|
||||||
"version": "12.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
|
||||||
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@types/linkify-it": "*",
|
|
||||||
"@types/mdurl": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/mdurl": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "17.0.32",
|
"version": "17.0.32",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz",
|
||||||
@@ -3583,6 +3594,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/socket.io-client": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-s+IPvFoEIjKA3RdJz/Z2dGR4gLgysKi8owcnrVwNjgvc01Lk68LJDDsG2GRqegFITcxmvCMYM7bhMpwEMlHmDg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"socket.io-client": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@typescript-eslint/eslint-plugin": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.23.0",
|
"version": "5.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz",
|
||||||
@@ -3762,7 +3782,8 @@
|
|||||||
"argparse": {
|
"argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"array-union": {
|
"array-union": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
@@ -3947,7 +3968,6 @@
|
|||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
}
|
}
|
||||||
@@ -4028,10 +4048,22 @@
|
|||||||
"integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==",
|
"integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"entities": {
|
"engine.io-client": {
|
||||||
"version": "3.0.1",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
|
||||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="
|
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
|
||||||
|
"requires": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.0.3",
|
||||||
|
"ws": "~8.2.3",
|
||||||
|
"xmlhttprequest-ssl": "~2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"engine.io-parser": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
|
||||||
},
|
},
|
||||||
"es6-promise": {
|
"es6-promise": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
@@ -4399,6 +4431,11 @@
|
|||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"exifr": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="
|
||||||
|
},
|
||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -4729,14 +4766,6 @@
|
|||||||
"integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
|
"integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"linkify-it": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
|
||||||
"requires": {
|
|
||||||
"uc.micro": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
@@ -4771,23 +4800,6 @@
|
|||||||
"sourcemap-codec": "^1.4.8"
|
"sourcemap-codec": "^1.4.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"markdown-it": {
|
|
||||||
"version": "13.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
|
||||||
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
|
||||||
"requires": {
|
|
||||||
"argparse": "^2.0.1",
|
|
||||||
"entities": "~3.0.1",
|
|
||||||
"linkify-it": "^4.0.1",
|
|
||||||
"mdurl": "^1.0.1",
|
|
||||||
"uc.micro": "^1.0.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mdurl": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
|
|
||||||
},
|
|
||||||
"merge2": {
|
"merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -4867,8 +4879,7 @@
|
|||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"nanoid": {
|
"nanoid": {
|
||||||
"version": "3.3.4",
|
"version": "3.3.4",
|
||||||
@@ -5199,6 +5210,26 @@
|
|||||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"socket.io-client": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-e6nLVgiRYatS+AHXnOnGi4ocOpubvOUCGhyWw8v+/FxW8saHkinG6Dfhi9TU0Kt/8mwJIAASxvw6eujQmjdZVA==",
|
||||||
|
"requires": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.2.1",
|
||||||
|
"socket.io-parser": "~4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"socket.io-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-tLfmEwcEwnlQTxFB7jibL/q2+q8dlVQzj4JdRLJ/W/G1+Fu9VSxCx1Lo+n1HvXxKnM//dUuD0xgiA7tQf57Vng==",
|
||||||
|
"requires": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sorcery": {
|
"sorcery": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
|
||||||
@@ -5436,11 +5467,6 @@
|
|||||||
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
|
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"uc.micro": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
|
||||||
},
|
|
||||||
"uri-js": {
|
"uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
@@ -5506,6 +5532,17 @@
|
|||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "8.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||||
|
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
|
"xmlhttprequest-ssl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
|
||||||
|
},
|
||||||
"xtend": {
|
"xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "svelte-kit dev --host 0.0.0.0 --port 3002",
|
"dev": "svelte-kit dev --host 0.0.0.0",
|
||||||
"build": "svelte-kit build",
|
"build": "svelte-kit build",
|
||||||
"package": "svelte-kit package",
|
"package": "svelte-kit package",
|
||||||
"preview": "svelte-kit preview",
|
"preview": "svelte-kit preview",
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"@types/leaflet": "^1.7.10",
|
"@types/leaflet": "^1.7.10",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
|
"@types/socket.io-client": "^3.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||||
"@typescript-eslint/parser": "^5.10.1",
|
"@typescript-eslint/parser": "^5.10.1",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
@@ -43,10 +44,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"cookie": "^0.4.2",
|
"cookie": "^0.4.2",
|
||||||
|
"exifr": "^7.1.3",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"moment": "^2.29.3",
|
"moment": "^2.29.3",
|
||||||
|
"socket.io-client": "^4.5.1",
|
||||||
"svelte-material-icons": "^2.0.2"
|
"svelte-material-icons": "^2.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
<Close size="24" color="#232323" />
|
<Close size="24" color="#232323" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p class="text-black text-lg">Info</p>
|
<p class="text-immich-fg text-lg">Info</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-4 py-4">
|
<div class="px-4 py-4">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
const loadVideoData = async () => {
|
const loadVideoData = async () => {
|
||||||
isThumbnailVideoPlaying = false;
|
isThumbnailVideoPlaying = false;
|
||||||
const videoUrl = `/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isWeb=true`;
|
const videoUrl = `/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isWeb=true`;
|
||||||
|
|
||||||
if ($session.user) {
|
if ($session.user) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(serverEndpoint + videoUrl, {
|
const res = await fetch(serverEndpoint + videoUrl, {
|
||||||
@@ -55,16 +56,20 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
videoData = URL.createObjectURL(await res.blob());
|
videoData = URL.createObjectURL(await res.blob());
|
||||||
|
|
||||||
videoPlayerNode.src = videoData;
|
videoPlayerNode.src = videoData;
|
||||||
|
// videoPlayerNode.src = videoData + '#t=0,5';
|
||||||
|
|
||||||
videoPlayerNode.load();
|
videoPlayerNode.load();
|
||||||
|
|
||||||
videoPlayerNode.onloadeddata = () => {
|
videoPlayerNode.onloadeddata = () => {
|
||||||
console.log('first frame load');
|
console.log('first frame load');
|
||||||
};
|
};
|
||||||
|
|
||||||
videoPlayerNode.oncanplaythrough = () => {
|
videoPlayerNode.oncanplaythrough = () => {
|
||||||
console.log('can play through');
|
console.log('can play through');
|
||||||
};
|
};
|
||||||
|
|
||||||
videoPlayerNode.oncanplay = () => {
|
videoPlayerNode.oncanplay = () => {
|
||||||
console.log('can play');
|
console.log('can play');
|
||||||
videoPlayerNode.muted = true;
|
videoPlayerNode.muted = true;
|
||||||
|
|||||||
@@ -2,16 +2,19 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import type { ImmichUser } from '$lib/models/immich-user';
|
import type { ImmichUser } from '$lib/models/immich-user';
|
||||||
import { onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade, fly, slide } from 'svelte/transition';
|
||||||
import { postRequest } from '../../api';
|
import { postRequest } from '../../api';
|
||||||
import { serverEndpoint } from '../../constants';
|
import { serverEndpoint } from '../../constants';
|
||||||
|
import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
|
||||||
import { clickOutside } from './click-outside';
|
import { clickOutside } from './click-outside';
|
||||||
|
|
||||||
export let user: ImmichUser;
|
export let user: ImmichUser;
|
||||||
|
|
||||||
let shouldShowAccountInfo = false;
|
let shouldShowAccountInfo = false;
|
||||||
let shouldShowProfileImage = false;
|
let shouldShowProfileImage = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
let shouldShowAccountInfoPanel = false;
|
let shouldShowAccountInfoPanel = false;
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const res = await fetch(`${serverEndpoint}/user/profile-image/${user.id}`, { method: 'GET' });
|
const res = await fetch(`${serverEndpoint}/user/profile-image/${user.id}`, { method: 'GET' });
|
||||||
@@ -41,7 +44,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section id="dashboard-navbar" class="fixed w-screen z-[100] bg-immich-bg text-sm">
|
<section id="dashboard-navbar" class="fixed w-screen z-[100] bg-immich-bg text-sm">
|
||||||
<div class="flex border place-items-center px-6 py-2 ">
|
<div class="flex border-b place-items-center px-6 py-2 ">
|
||||||
<a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
|
<a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
|
||||||
<img src="/immich-logo.svg" alt="immich logo" height="35" width="35" />
|
<img src="/immich-logo.svg" alt="immich logo" height="35" width="35" />
|
||||||
<h1 class="font-immich-title text-2xl text-immich-primary">IMMICH</h1>
|
<h1 class="font-immich-title text-2xl text-immich-primary">IMMICH</h1>
|
||||||
@@ -49,13 +52,21 @@
|
|||||||
<div class="flex-1 ml-24">
|
<div class="flex-1 ml-24">
|
||||||
<input class="w-[50%] border rounded-md bg-gray-200 px-8 py-4" placeholder="Search - Coming soon" />
|
<input class="w-[50%] border rounded-md bg-gray-200 px-8 py-4" placeholder="Search - Coming soon" />
|
||||||
</div>
|
</div>
|
||||||
|
<section class="flex gap-4 place-items-center">
|
||||||
<section class="flex gap-6 place-items-center">
|
{#if $page.url.pathname !== '/admin'}
|
||||||
<!-- <div>Upload</div> -->
|
<button
|
||||||
|
in:fly={{ x: 50, duration: 250 }}
|
||||||
|
on:click={() => dispatch('uploadClicked')}
|
||||||
|
class="flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
<TrayArrowUp size="20" />
|
||||||
|
<span> Upload </span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if user.isAdmin}
|
{#if user.isAdmin}
|
||||||
<button
|
<button
|
||||||
class={`hover:text-immich-primary font-medium ${
|
class={`flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium ${
|
||||||
$page.url.pathname == '/admin' && 'text-immich-primary underline'
|
$page.url.pathname == '/admin' && 'text-immich-primary underline'
|
||||||
}`}
|
}`}
|
||||||
on:click={navigateToAdmin}>Administration</button
|
on:click={navigateToAdmin}>Administration</button
|
||||||
|
|||||||
191
web/src/lib/components/shared/upload-panel.svelte
Normal file
191
web/src/lib/components/shared/upload-panel.svelte
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { quartInOut } from 'svelte/easing';
|
||||||
|
import { scale, fade } from 'svelte/transition';
|
||||||
|
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||||
|
import CloudUploadOutline from 'svelte-material-icons/CloudUploadOutline.svelte';
|
||||||
|
import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte';
|
||||||
|
import type { UploadAsset } from '$lib/models/upload-asset';
|
||||||
|
|
||||||
|
let showDetail = true;
|
||||||
|
|
||||||
|
let uploadLength = 0;
|
||||||
|
|
||||||
|
const showUploadImageThumbnail = async (a: UploadAsset) => {
|
||||||
|
const extension = a.fileExtension.toLowerCase();
|
||||||
|
|
||||||
|
if (extension == 'jpeg' || extension == 'jpg' || extension == 'png') {
|
||||||
|
try {
|
||||||
|
const imgData = await a.file.arrayBuffer();
|
||||||
|
const arrayBufferView = new Uint8Array(imgData);
|
||||||
|
const blob = new Blob([arrayBufferView], { type: 'image/jpeg' });
|
||||||
|
const urlCreator = window.URL || window.webkitURL;
|
||||||
|
const imageUrl = urlCreator.createObjectURL(blob);
|
||||||
|
const img: any = document.getElementById(`${a.id}`);
|
||||||
|
img.src = imageUrl;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSizeInHumanReadableFormat(sizeInByte: number) {
|
||||||
|
const pepibyte = 1.126 * Math.pow(10, 15);
|
||||||
|
const tebibyte = 1.1 * Math.pow(10, 12);
|
||||||
|
const gibibyte = 1.074 * Math.pow(10, 9);
|
||||||
|
const mebibyte = 1.049 * Math.pow(10, 6);
|
||||||
|
const kibibyte = 1024;
|
||||||
|
// Pebibyte
|
||||||
|
if (sizeInByte >= pepibyte) {
|
||||||
|
// Pe
|
||||||
|
return `${(sizeInByte / pepibyte).toFixed(1)}PB`;
|
||||||
|
} else if (tebibyte <= sizeInByte && sizeInByte < pepibyte) {
|
||||||
|
// Te
|
||||||
|
return `${(sizeInByte / tebibyte).toFixed(1)}TB`;
|
||||||
|
} else if (gibibyte <= sizeInByte && sizeInByte < tebibyte) {
|
||||||
|
// Gi
|
||||||
|
return `${(sizeInByte / gibibyte).toFixed(1)}GB`;
|
||||||
|
} else if (mebibyte <= sizeInByte && sizeInByte < gibibyte) {
|
||||||
|
// Mega
|
||||||
|
return `${(sizeInByte / mebibyte).toFixed(1)}MB`;
|
||||||
|
} else if (kibibyte <= sizeInByte && sizeInByte < mebibyte) {
|
||||||
|
// Kibi
|
||||||
|
return `${(sizeInByte / kibibyte).toFixed(1)}KB`;
|
||||||
|
} else {
|
||||||
|
return `${sizeInByte}B`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactive action to get thumbnail image of upload asset whenever there is a new one added to the list
|
||||||
|
$: {
|
||||||
|
if ($uploadAssetsStore.length != uploadLength) {
|
||||||
|
$uploadAssetsStore.map((asset) => {
|
||||||
|
showUploadImageThumbnail(asset);
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadLength = $uploadAssetsStore.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (showDetail) {
|
||||||
|
$uploadAssetsStore.map((asset) => {
|
||||||
|
showUploadImageThumbnail(asset);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isUploading = false;
|
||||||
|
|
||||||
|
uploadAssetsStore.isUploading.subscribe((value) => (isUploading = value));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isUploading}
|
||||||
|
<div
|
||||||
|
in:fade={{ duration: 250 }}
|
||||||
|
out:fade={{ duration: 250, delay: 1000 }}
|
||||||
|
class="absolute right-6 bottom-6 z-[10000]"
|
||||||
|
>
|
||||||
|
{#if showDetail}
|
||||||
|
<div
|
||||||
|
in:scale={{ duration: 250, easing: quartInOut }}
|
||||||
|
class="bg-gray-200 p-4 text-sm w-[300px] rounded-lg shadow-sm border "
|
||||||
|
>
|
||||||
|
<div class="flex justify-between place-item-center mb-4">
|
||||||
|
<p class="text-xs text-gray-500">UPLOADING {$uploadAssetsStore.length}</p>
|
||||||
|
<button
|
||||||
|
on:click={() => (showDetail = false)}
|
||||||
|
class="w-[20px] h-[20px] bg-gray-50 rounded-full flex place-items-center place-content-center transition-colors hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<WindowMinimize />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="upload-item-list" class="max-h-[400px] overflow-y-auto pr-2 rounded-lg">
|
||||||
|
{#each $uploadAssetsStore as uploadAsset}
|
||||||
|
<div
|
||||||
|
in:fade={{ duration: 250 }}
|
||||||
|
out:fade={{ duration: 100 }}
|
||||||
|
class="text-xs mt-3 rounded-lg bg-immich-bg grid grid-cols-[70px_auto] gap-2 h-[70px]"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<img
|
||||||
|
in:fade={{ duration: 250 }}
|
||||||
|
id={`${uploadAsset.id}`}
|
||||||
|
src="/immich-logo.svg"
|
||||||
|
alt=""
|
||||||
|
class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg "
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="bottom-0 left-0 absolute w-full h-[25px] bg-immich-primary/30">
|
||||||
|
<p
|
||||||
|
class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase"
|
||||||
|
>
|
||||||
|
.{uploadAsset.fileExtension}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2 pr-4 flex flex-col justify-between">
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
|
||||||
|
value={`[${getSizeInHumanReadableFormat(uploadAsset.file.size)}] ${uploadAsset.file.name}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
|
||||||
|
<div
|
||||||
|
class="bg-immich-primary h-[15px] rounded-md transition-all"
|
||||||
|
style={`width: ${uploadAsset.progress}%`}
|
||||||
|
/>
|
||||||
|
<p class="absolute h-full w-full text-center top-0 text-[10px] ">{uploadAsset.progress}/100</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="rounded-full">
|
||||||
|
<button
|
||||||
|
in:scale={{ duration: 250, easing: quartInOut }}
|
||||||
|
on:click={() => (showDetail = true)}
|
||||||
|
class="absolute -top-4 -left-4 text-xs rounded-full w-10 h-10 p-5 flex place-items-center place-content-center bg-immich-primary text-gray-200"
|
||||||
|
>
|
||||||
|
{$uploadAssetsStore.length}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
in:scale={{ duration: 250, easing: quartInOut }}
|
||||||
|
on:click={() => (showDetail = true)}
|
||||||
|
class="bg-gray-300 p-5 rounded-full w-16 h-16 flex place-items-center place-content-center text-sm shadow-lg "
|
||||||
|
>
|
||||||
|
<div class="animate-pulse">
|
||||||
|
<CloudUploadOutline size="30" color="#4250af" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* width */
|
||||||
|
#upload-item-list::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Track */
|
||||||
|
#upload-item-list::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle */
|
||||||
|
#upload-item-list::-webkit-scrollbar-thumb {
|
||||||
|
background: #4250af68;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle on hover */
|
||||||
|
#upload-item-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #4250afad;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1 +1 @@
|
|||||||
export const serverEndpoint = import.meta.env.VITE_SERVER_ENDPOINT
|
export const serverEndpoint: string = import.meta.env.VITE_SERVER_ENDPOINT;
|
||||||
|
|||||||
6
web/src/lib/models/upload-asset.ts
Normal file
6
web/src/lib/models/upload-asset.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type UploadAsset = {
|
||||||
|
id: string;
|
||||||
|
file: File;
|
||||||
|
progress: number;
|
||||||
|
fileExtension: string;
|
||||||
|
};
|
||||||
@@ -1,33 +1,29 @@
|
|||||||
import { writable, derived } from 'svelte/store';
|
import { writable, derived } from 'svelte/store';
|
||||||
import { getRequest } from '$lib/api';
|
import { getRequest } from '$lib/api';
|
||||||
import type { ImmichAsset } from '$lib/models/immich-asset'
|
import type { ImmichAsset } from '$lib/models/immich-asset';
|
||||||
import lodash from 'lodash-es';
|
import lodash from 'lodash-es';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
export const assets = writable<ImmichAsset[]>([]);
|
export const assets = writable<ImmichAsset[]>([]);
|
||||||
|
|
||||||
export const assetsGroupByDate = derived(assets, ($assets) => {
|
export const assetsGroupByDate = derived(assets, ($assets) => {
|
||||||
|
try {
|
||||||
try {
|
return lodash
|
||||||
return lodash.chain($assets)
|
.chain($assets)
|
||||||
.groupBy((a) => moment(a.createdAt).format('ddd, MMM DD'))
|
.groupBy((a) => moment(a.createdAt).format('ddd, MMM DD'))
|
||||||
.sortBy((group) => $assets.indexOf(group[0]))
|
.sortBy((group) => $assets.indexOf(group[0]))
|
||||||
.value();
|
.value();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("error deriving state assets", e)
|
console.log('error deriving state assets', e);
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
|
});
|
||||||
})
|
|
||||||
|
|
||||||
export const flattenAssetGroupByDate = derived(assetsGroupByDate, ($assetsGroupByDate) => {
|
export const flattenAssetGroupByDate = derived(assetsGroupByDate, ($assetsGroupByDate) => {
|
||||||
return $assetsGroupByDate.flat();
|
return $assetsGroupByDate.flat();
|
||||||
})
|
});
|
||||||
|
|
||||||
export const getAssetsInfo = async (accessToken: string) => {
|
export const getAssetsInfo = async (accessToken: string) => {
|
||||||
const res = await getRequest('asset', accessToken);
|
const res = await getRequest('asset', accessToken);
|
||||||
|
assets.set(res);
|
||||||
assets.set(res);
|
};
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
45
web/src/lib/stores/upload.ts
Normal file
45
web/src/lib/stores/upload.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { writable, derived } from 'svelte/store';
|
||||||
|
import type { UploadAsset } from '../models/upload-asset';
|
||||||
|
|
||||||
|
function createUploadStore() {
|
||||||
|
const uploadAssets = writable<Array<UploadAsset>>([]);
|
||||||
|
|
||||||
|
const { subscribe } = uploadAssets;
|
||||||
|
|
||||||
|
const isUploading = derived(uploadAssets, ($uploadAssets) => {
|
||||||
|
return $uploadAssets.length > 0 ? true : false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addNewUploadAsset = (newAsset: UploadAsset) => {
|
||||||
|
uploadAssets.update((currentSet) => [...currentSet, newAsset]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProgress = (id: string, progress: number) => {
|
||||||
|
uploadAssets.update((uploadingAssets) => {
|
||||||
|
return uploadingAssets.map((asset) => {
|
||||||
|
if (asset.id == id) {
|
||||||
|
return {
|
||||||
|
...asset,
|
||||||
|
progress: progress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return asset;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUploadAsset = (id: string) => {
|
||||||
|
uploadAssets.update((uploadingAsset) => uploadingAsset.filter((a) => a.id != id));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
isUploading,
|
||||||
|
addNewUploadAsset,
|
||||||
|
updateProgress,
|
||||||
|
removeUploadAsset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadAssetsStore = createUploadStore();
|
||||||
36
web/src/lib/stores/websocket.ts
Normal file
36
web/src/lib/stores/websocket.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Socket, io } from 'socket.io-client';
|
||||||
|
import { serverEndpoint } from '../constants';
|
||||||
|
import type { ImmichAsset } from '../models/immich-asset';
|
||||||
|
import { assets } from './assets';
|
||||||
|
|
||||||
|
export const openWebsocketConnection = (accessToken: string) => {
|
||||||
|
const websocketEndpoint = serverEndpoint.replace('/api', '');
|
||||||
|
try {
|
||||||
|
const websocket = io(websocketEndpoint, {
|
||||||
|
path: '/api/socket.io',
|
||||||
|
transports: ['polling'],
|
||||||
|
reconnection: true,
|
||||||
|
forceNew: true,
|
||||||
|
autoConnect: true,
|
||||||
|
extraHeaders: {
|
||||||
|
Authorization: 'Bearer ' + accessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
listenToEvent(websocket);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Cannot connect to websocket ', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const listenToEvent = (socket: Socket) => {
|
||||||
|
socket.on('on_upload_success', (data) => {
|
||||||
|
const newUploadedAsset: ImmichAsset = JSON.parse(data);
|
||||||
|
|
||||||
|
assets.update((assets) => [...assets, newUploadedAsset]);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (e) => {
|
||||||
|
console.log('Websocket Error', e);
|
||||||
|
});
|
||||||
|
};
|
||||||
113
web/src/lib/utils/file-uploader.ts
Normal file
113
web/src/lib/utils/file-uploader.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import * as exifr from 'exifr';
|
||||||
|
import { serverEndpoint } from '../constants';
|
||||||
|
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||||
|
import type { UploadAsset } from '../models/upload-asset';
|
||||||
|
|
||||||
|
export async function fileUploader(asset: File, accessToken: string) {
|
||||||
|
const assetType = asset.type.split('/')[0].toUpperCase();
|
||||||
|
const temp = asset.name.split('.');
|
||||||
|
const fileExtension = temp[temp.length - 1];
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let exifData = null;
|
||||||
|
|
||||||
|
if (assetType !== 'VIDEO') {
|
||||||
|
exifData = await exifr.parse(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdAt =
|
||||||
|
exifData && exifData.DateTimeOriginal != null
|
||||||
|
? new Date(exifData.DateTimeOriginal).toISOString()
|
||||||
|
: new Date(asset.lastModified).toISOString();
|
||||||
|
|
||||||
|
const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
|
||||||
|
|
||||||
|
// Create and add Unique ID of asset on the device
|
||||||
|
formData.append('deviceAssetId', deviceAssetId);
|
||||||
|
|
||||||
|
// Get device id - for web -> use WEB
|
||||||
|
formData.append('deviceId', 'WEB');
|
||||||
|
|
||||||
|
// Get asset type
|
||||||
|
formData.append('assetType', assetType);
|
||||||
|
|
||||||
|
// Get Asset Created Date
|
||||||
|
formData.append('createdAt', createdAt);
|
||||||
|
|
||||||
|
// Get Asset Modified At
|
||||||
|
formData.append('modifiedAt', new Date(asset.lastModified).toISOString());
|
||||||
|
|
||||||
|
// Set Asset is Favorite to false
|
||||||
|
formData.append('isFavorite', 'false');
|
||||||
|
|
||||||
|
// Get asset duration
|
||||||
|
formData.append('duration', '0:00:00.000000');
|
||||||
|
|
||||||
|
// Get asset file extension
|
||||||
|
formData.append('fileExtension', '.' + fileExtension);
|
||||||
|
|
||||||
|
// Get asset binary data.
|
||||||
|
formData.append('assetData', asset);
|
||||||
|
|
||||||
|
// Check if asset upload on server before performing upload
|
||||||
|
const res = await fetch(serverEndpoint + '/asset/check', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ deviceAssetId }),
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + accessToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
const { isExist } = await res.json();
|
||||||
|
|
||||||
|
if (isExist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
|
||||||
|
request.upload.onloadstart = () => {
|
||||||
|
const newUploadAsset: UploadAsset = {
|
||||||
|
id: deviceAssetId,
|
||||||
|
file: asset,
|
||||||
|
progress: 0,
|
||||||
|
fileExtension: fileExtension,
|
||||||
|
};
|
||||||
|
|
||||||
|
uploadAssetsStore.addNewUploadAsset(newUploadAsset);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.upload.onload = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||||
|
}, 2500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// listen for `error` event
|
||||||
|
request.upload.onerror = () => {
|
||||||
|
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// listen for `abort` event
|
||||||
|
request.upload.onabort = () => {
|
||||||
|
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// listen for `progress` event
|
||||||
|
request.upload.onprogress = (event) => {
|
||||||
|
const percentComplete = Math.floor((event.loaded / event.total) * 100);
|
||||||
|
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.open('POST', `${serverEndpoint}/asset/upload`);
|
||||||
|
request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||||
|
|
||||||
|
request.send(formData);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('error uploading file ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
import { blur } from 'svelte/transition';
|
import { blur } from 'svelte/transition';
|
||||||
|
|
||||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||||
import FullScreenModal from '../lib/components/shared/full-screen-modal.svelte';
|
import AnnouncementBox from '$lib/components/shared/announcement-box.svelte';
|
||||||
import AnnouncementBox from '../lib/components/shared/announcement-box.svelte';
|
import UploadPanel from '$lib/components/shared/upload-panel.svelte';
|
||||||
|
|
||||||
export let url: string;
|
export let url: string;
|
||||||
export let shouldShowAnnouncement: boolean;
|
export let shouldShowAnnouncement: boolean;
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<div transition:blur={{ duration: 250 }}>
|
<div transition:blur={{ duration: 250 }}>
|
||||||
<slot />
|
<slot />
|
||||||
<DownloadPanel />
|
<DownloadPanel />
|
||||||
|
<UploadPanel />
|
||||||
{#if shouldShowAnnouncement}
|
{#if shouldShowAnnouncement}
|
||||||
<AnnouncementBox {localVersion} {remoteVersion} on:close={() => (shouldShowAnnouncement = false)} />
|
<AnnouncementBox {localVersion} {remoteVersion} on:close={() => (shouldShowAnnouncement = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { serverEndpoint } from '$lib/constants';
|
import { serverEndpoint } from '$lib/constants';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export let isAdminUserExist: boolean;
|
export let isAdminUserExist: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
import AssetViewer from '../../lib/components/asset-viewer/asset-viewer.svelte';
|
import AssetViewer from '../../lib/components/asset-viewer/asset-viewer.svelte';
|
||||||
import DownloadPanel from '../../lib/components/asset-viewer/download-panel.svelte';
|
import DownloadPanel from '../../lib/components/asset-viewer/download-panel.svelte';
|
||||||
import StatusBox from '../../lib/components/shared/status-box.svelte';
|
import StatusBox from '../../lib/components/shared/status-box.svelte';
|
||||||
|
import { fileUploader } from '../../lib/utils/file-uploader';
|
||||||
|
import { openWebsocketConnection } from '../../lib/stores/websocket';
|
||||||
|
|
||||||
export let user: ImmichUser;
|
export let user: ImmichUser;
|
||||||
let selectedAction: AppSideBarSelection;
|
let selectedAction: AppSideBarSelection;
|
||||||
@@ -64,6 +66,8 @@
|
|||||||
|
|
||||||
if ($session.user) {
|
if ($session.user) {
|
||||||
await getAssetsInfo($session.user.accessToken);
|
await getAssetsInfo($session.user.accessToken);
|
||||||
|
|
||||||
|
openWebsocketConnection($session.user.accessToken);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,7 +83,34 @@
|
|||||||
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId);
|
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId);
|
||||||
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
||||||
isShowAsset = true;
|
isShowAsset = true;
|
||||||
// pushState(assetId);
|
};
|
||||||
|
|
||||||
|
const uploadClickedHandler = async () => {
|
||||||
|
if ($session.user) {
|
||||||
|
try {
|
||||||
|
let fileSelector = document.createElement('input');
|
||||||
|
|
||||||
|
fileSelector.type = 'file';
|
||||||
|
fileSelector.multiple = true;
|
||||||
|
fileSelector.accept = 'image/*,video/*,.heic,.heif';
|
||||||
|
|
||||||
|
fileSelector.onchange = async (e: any) => {
|
||||||
|
const files = Array.from<File>(e.target.files);
|
||||||
|
|
||||||
|
const acceptedFile = files.filter(
|
||||||
|
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const asset of acceptedFile) {
|
||||||
|
await fileUploader(asset, $session.user!.accessToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSelector.click();
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error seelcting file', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -88,10 +119,10 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<NavigationBar {user} />
|
<NavigationBar {user} on:uploadClicked={uploadClickedHandler} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
|
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<section id="sidebar" class="flex flex-col gap-4 pt-8 pr-6">
|
<section id="sidebar" class="flex flex-col gap-4 pt-8 pr-6">
|
||||||
<SideBarButton
|
<SideBarButton
|
||||||
@@ -111,7 +142,7 @@
|
|||||||
|
|
||||||
<!-- Main Section -->
|
<!-- Main Section -->
|
||||||
<section class="overflow-y-auto relative">
|
<section class="overflow-y-auto relative">
|
||||||
<section id="assets-content" class="relative pt-8 pl-4">
|
<section id="assets-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
|
||||||
<section id="image-grid" class="flex flex-wrap gap-14">
|
<section id="image-grid" class="flex flex-wrap gap-14">
|
||||||
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex}
|
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex}
|
||||||
<!-- Asset Group By Date -->
|
<!-- Asset Group By Date -->
|
||||||
@@ -121,7 +152,7 @@
|
|||||||
on:mouseleave={() => (isMouseOverGroup = false)}
|
on:mouseleave={() => (isMouseOverGroup = false)}
|
||||||
>
|
>
|
||||||
<!-- Date group title -->
|
<!-- Date group title -->
|
||||||
<p class="font-medium text-sm text-black mb-2 flex place-items-center h-6">
|
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
|
||||||
{#if selectedGroupThumbnail === groupIndex && isMouseOverGroup}
|
{#if selectedGroupThumbnail === groupIndex && isMouseOverGroup}
|
||||||
<div
|
<div
|
||||||
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||||
@@ -136,7 +167,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-[2px]">
|
||||||
{#each assetsInDateGroup as asset}
|
{#each assetsInDateGroup as asset}
|
||||||
<ImmichThumbnail
|
<ImmichThumbnail
|
||||||
{asset}
|
{asset}
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ const config = {
|
|||||||
methodOverride: {
|
methodOverride: {
|
||||||
allowed: ['PATCH', 'DELETE'],
|
allowed: ['PATCH', 'DELETE'],
|
||||||
},
|
},
|
||||||
|
vite: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ module.exports = {
|
|||||||
colors: {
|
colors: {
|
||||||
'immich-primary': '#4250af',
|
'immich-primary': '#4250af',
|
||||||
'immich-bg': '#f6f8fe',
|
'immich-bg': '#f6f8fe',
|
||||||
|
'immich-fg': 'black',
|
||||||
|
|
||||||
|
// 'immich-bg': '#121212',
|
||||||
|
// 'immich-fg': '#D0D0D0',
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
'immich-title': ['Snowburst One', 'cursive'],
|
'immich-title': ['Snowburst One', 'cursive'],
|
||||||
|
|||||||
Reference in New Issue
Block a user