Compare commits
22 Commits
v1.3.1-dev
...
v1.5.1+9-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b69f6e0df7 | ||
|
|
be2794a372 | ||
|
|
2ff25b49f4 | ||
|
|
135d72d4cd | ||
|
|
90ef64efa3 | ||
|
|
60df387459 | ||
|
|
fc1acf6f01 | ||
|
|
cfc5229964 | ||
|
|
f9ddeac265 | ||
|
|
8d7c576037 | ||
|
|
fccdbdd66a | ||
|
|
23ba651705 | ||
|
|
ac0ad98b55 | ||
|
|
9cbd5d1b0c | ||
|
|
80fd664cc8 | ||
|
|
041c711cb9 | ||
|
|
dd9c5244fd | ||
|
|
fe693db84f | ||
|
|
5c9d3cd08b | ||
|
|
725ab5622f | ||
|
|
e9acd21733 | ||
|
|
ce1ab1ed50 |
@@ -1,10 +1,6 @@
|
|||||||
name: Build Server
|
name: Build Server - Latest
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Triggers the workflow on push or pull request events but only for the main branch
|
|
||||||
#schedule:
|
|
||||||
# * is a special character in YAML so you have to quote this string
|
|
||||||
#- cron: '0 0 * * *'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
38
.github/workflows/build_push_server_release.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Build Server - Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
buildandpush:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: "main" # branch
|
||||||
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1.2.0
|
||||||
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1.6.0
|
||||||
|
# https://github.com/docker/login-action#docker-hub
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
|
- name: Build and push Immich
|
||||||
|
uses: docker/build-push-action@v2.10.0
|
||||||
|
with:
|
||||||
|
context: ./server
|
||||||
|
file: ./server/Dockerfile
|
||||||
|
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||||
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
|
pull: true
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-server:${{github.ref_name}}
|
||||||
4
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
12
Makefile
@@ -1,8 +1,14 @@
|
|||||||
dev:
|
dev:
|
||||||
docker-compose -f ./docker/docker-compose.yml up
|
docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
dev-update:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V
|
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
dev-scale:
|
dev-scale:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich_server=3 --remove-orphans
|
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans
|
||||||
|
|
||||||
|
prod:
|
||||||
|
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
prod-scale:
|
||||||
|
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich_server=3 --scale immich_microservices=3 --remove-orphans
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
# Deployment checklist for iOS/Android/Server
|
# Deployment checklist for iOS/Android/Server
|
||||||
|
|
||||||
[] Up version in [mobile/pubspec.yml](/mobile/pubspec.yaml)
|
[ ] Up version in [mobile/pubspec.yml](/mobile/pubspec.yaml)
|
||||||
|
|
||||||
[] Up version in [docker/docker-compose.yml](/docker/docker-compose.yml) for `immich_server` service
|
[ ] Up version in [docker/docker-compose.yml](/docker/docker-compose.yml) for `immich_server` service
|
||||||
|
|
||||||
[] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
[ ] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
||||||
|
|
||||||
[] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
[ ] Up version in [docker/docker-compose.dev.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
||||||
|
|
||||||
[] Up version in iOS Fastlane [/mobile/ios/fastlane/Fastfile](/mobile/ios/fastlane/Fastfile)
|
[ ] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
||||||
|
|
||||||
|
[ ] Up version in iOS Fastlane [/mobile/ios/fastlane/Fastfile](/mobile/ios/fastlane/Fastfile)
|
||||||
|
|
||||||
|
[ ] Add changelog to [Android Fastlane F-droid folder](/mobile/android/fastlane/metadata/android/en-US/changelogs)
|
||||||
|
|
||||||
All of the version should be the same.
|
All of the version should be the same.
|
||||||
@@ -34,7 +34,8 @@ Loading ~4000 images/videos
|
|||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/nsc1.png" width="150" title="Login With Custom URL">
|
<img src="design/nsc1.png" width="150" title="Login With Custom URL">
|
||||||
<img src="design/nsc2.png" width="150" title="Backup Setting Info">
|
<img src="design/nsc2.png" width="150" title="Backup Setting Info">
|
||||||
<img src="design/nsc3.png" width="150" title="Multiple seelct">
|
<img src="design/nsc3.png" width="150" title="Multiple select">
|
||||||
|
<img src="design/nsc4.jpeg" width="150" title="Curated Search Info">
|
||||||
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
@@ -55,11 +56,13 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
- Extract and display EXIF info.
|
- Extract and display EXIF info.
|
||||||
- Real-time render from multi-device upload event.
|
- Real-time render from multi-device upload event.
|
||||||
- Image Tagging/Classification based on ImageNet dataset
|
- 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)
|
- Search assets based on tags and exif data (lens, make, model, orientation)
|
||||||
- Upload assets from your local computer/server using [immich cli tools](https://www.npmjs.com/package/immich)
|
- Upload assets from your local computer/server using [immich cli tools](https://www.npmjs.com/package/immich)
|
||||||
- [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
- [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
||||||
- Show asset's location information on map (OpenStreetMap).
|
- Show asset's location information on map (OpenStreetMap).
|
||||||
- Show curated places on the search page
|
- Show curated places on the search page
|
||||||
|
- Show curated objects on the search page
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
@@ -69,7 +72,7 @@ You can use docker compose for development, there are several services that comp
|
|||||||
2. PostgreSQL
|
2. PostgreSQL
|
||||||
3. Redis
|
3. Redis
|
||||||
4. Nginx
|
4. Nginx
|
||||||
5. TensorFlow and Keras
|
5. TensorFlow
|
||||||
|
|
||||||
## Populate .env file
|
## Populate .env file
|
||||||
|
|
||||||
|
|||||||
BIN
design/nsc4.jpeg
Normal file
|
After Width: | Height: | Size: 406 KiB |
@@ -1,6 +1,3 @@
|
|||||||
# STAGE
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
|
|||||||
89
docker/docker-compose.dev.yml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
immich_server:
|
||||||
|
image: immich-server-dev:1.5.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
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- database
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
|
||||||
|
immich_microservices:
|
||||||
|
image: immich-microservices-dev:1.5.0
|
||||||
|
build:
|
||||||
|
context: ../microservices
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
command: npm run start:dev
|
||||||
|
expose:
|
||||||
|
- "3001"
|
||||||
|
volumes:
|
||||||
|
- ../microservices:/usr/src/app
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: immich_redis
|
||||||
|
image: redis:6.2
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
|
||||||
|
database:
|
||||||
|
container_name: immich_postgres
|
||||||
|
image: postgres:14
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
|
PG_DATA: /var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
container_name: proxy_nginx
|
||||||
|
image: nginx:latest
|
||||||
|
volumes:
|
||||||
|
- ./settings/nginx-conf:/etc/nginx/conf.d
|
||||||
|
ports:
|
||||||
|
- 2283:80
|
||||||
|
- 2284:443
|
||||||
|
logging:
|
||||||
|
driver: none
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
depends_on:
|
||||||
|
- immich_server
|
||||||
|
|
||||||
|
networks:
|
||||||
|
immich_network:
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
@@ -2,11 +2,10 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.3.0
|
image: immich-server-dev:1.5.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
target: development
|
dockerfile: Dockerfile
|
||||||
dockerfile: ../server/Dockerfile
|
|
||||||
command: npm run start:dev
|
command: npm run start:dev
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
@@ -22,6 +21,33 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich_network
|
||||||
|
|
||||||
|
immich_microservices:
|
||||||
|
image: immich-microservices-dev:1.5.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:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
@@ -60,35 +86,6 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich_server
|
||||||
|
|
||||||
immich_tf_fastapi:
|
|
||||||
container_name: immich_tf_fastapi
|
|
||||||
image: tensor_flow_fastapi:1.0.0
|
|
||||||
restart: always
|
|
||||||
command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload
|
|
||||||
build:
|
|
||||||
context: ../machine_learning
|
|
||||||
target: gpu
|
|
||||||
dockerfile: ../machine_learning/Dockerfile
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: 1
|
|
||||||
capabilities: [gpu]
|
|
||||||
volumes:
|
|
||||||
- ../machine_learning/app:/code/app
|
|
||||||
- ${UPLOAD_LOCATION}:/code/app/upload
|
|
||||||
ports:
|
|
||||||
- 2285:8000
|
|
||||||
expose:
|
|
||||||
- "8000"
|
|
||||||
depends_on:
|
|
||||||
- database
|
|
||||||
networks:
|
|
||||||
- immich_network
|
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich_network:
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.3.0
|
image: immich-server:1.5.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
target: development
|
dockerfile: Dockerfile
|
||||||
dockerfile: ../server/Dockerfile
|
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
# command: npm run start:dev
|
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -17,11 +15,36 @@ services:
|
|||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich_network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
immich_microservices:
|
||||||
|
image: immich-microservices:1.5.0
|
||||||
|
build:
|
||||||
|
context: ../microservices
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
|
expose:
|
||||||
|
- "3001"
|
||||||
|
volumes:
|
||||||
|
- ../microservices:/usr/src/app
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
networks:
|
||||||
|
- immich_network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
@@ -61,28 +84,28 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich_server
|
||||||
|
|
||||||
immich_tf_fastapi:
|
# immich_tf_fastapi:
|
||||||
container_name: immich_tf_fastapi
|
# container_name: immich_tf_fastapi
|
||||||
image: tensor_flow_fastapi:1.0.0
|
# image: tensor_flow_fastapi:1.0.0
|
||||||
restart: always
|
# restart: always
|
||||||
command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload
|
# command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload
|
||||||
build:
|
# build:
|
||||||
context: ../machine_learning
|
# context: ../machine_learning
|
||||||
target: cpu
|
# target: cpu
|
||||||
dockerfile: ../machine_learning/Dockerfile
|
# dockerfile: ../machine_learning/Dockerfile
|
||||||
volumes:
|
# volumes:
|
||||||
- ../machine_learning/app:/code/app
|
# - ../machine_learning/app:/code/app
|
||||||
- ${UPLOAD_LOCATION}:/code/app/upload
|
# - ${UPLOAD_LOCATION}:/code/app/upload
|
||||||
ports:
|
# ports:
|
||||||
- 2285:8000
|
# - 2285:8000
|
||||||
expose:
|
# expose:
|
||||||
- "8000"
|
# - "8000"
|
||||||
depends_on:
|
# depends_on:
|
||||||
- database
|
# - database
|
||||||
networks:
|
# networks:
|
||||||
- immich_network
|
# - immich_network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich_network:
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
@@ -13,7 +13,7 @@ server {
|
|||||||
client_max_body_size 50000M;
|
client_max_body_size 50000M;
|
||||||
|
|
||||||
listen 80;
|
listen 80;
|
||||||
|
access_log off;
|
||||||
location / {
|
location / {
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
proxy_buffer_size 16k;
|
proxy_buffer_size 16k;
|
||||||
|
|||||||
3
fastlane/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
This directory exists because of the F-Droid build process. F-Droid is using the same directory structure as Fastlane for the app metadata.
|
||||||
|
|
||||||
|
Because F-Droid expects the metadata to be located in the root of the repository we need to have this symlink.
|
||||||
1
fastlane/metadata
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../mobile/android/fastlane/metadata
|
||||||
4
microservices/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
upload/
|
||||||
|
dist/
|
||||||
|
|
||||||
24
microservices/.eslintrc.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
35
microservices/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
4
microservices/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
16
microservices/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:16-bullseye-slim
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
4
microservices/README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
# Microservices for Immich
|
||||||
|
|
||||||
|
## Image Classifier
|
||||||
2
microservices/entrypoint.sh
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# npm run typeorm migration:run
|
||||||
|
npm run build && npm run start:prod
|
||||||
4
microservices/nest-cli.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src"
|
||||||
|
}
|
||||||
17323
microservices/package-lock.json
generated
Normal file
83
microservices/package.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"name": "nest_microservices",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "rimraf dist",
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^8.0.0",
|
||||||
|
"@nestjs/core": "^8.0.0",
|
||||||
|
"@nestjs/mapped-types": "^1.0.1",
|
||||||
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
|
"@nestjs/typeorm": "^8.0.3",
|
||||||
|
"@tensorflow-models/coco-ssd": "^2.2.2",
|
||||||
|
"@tensorflow-models/mobilenet": "^2.1.0",
|
||||||
|
"@tensorflow/tfjs": "^3.15.0",
|
||||||
|
"@tensorflow/tfjs-converter": "^3.15.0",
|
||||||
|
"@tensorflow/tfjs-core": "^3.15.0",
|
||||||
|
"@tensorflow/tfjs-node": "^3.15.0",
|
||||||
|
"@tensorflow/tfjs-node-gpu": "^3.15.0",
|
||||||
|
"@trpc/server": "^9.20.3",
|
||||||
|
"pg": "^8.7.3",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"rxjs": "^7.2.0",
|
||||||
|
"typeorm": "^0.2.45"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^8.2.4",
|
||||||
|
"@nestjs/schematics": "^8.0.0",
|
||||||
|
"@nestjs/testing": "^8.0.0",
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/jest": "27.4.1",
|
||||||
|
"@types/node": "^16.0.0",
|
||||||
|
"@types/supertest": "^2.0.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
"eslint": "^8.0.1",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"jest": "^27.2.5",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"source-map-support": "^0.5.20",
|
||||||
|
"supertest": "^6.1.3",
|
||||||
|
"ts-jest": "^27.0.3",
|
||||||
|
"ts-loader": "^9.2.3",
|
||||||
|
"ts-node": "^10.0.0",
|
||||||
|
"tsconfig-paths": "^3.10.1",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
microservices/src/app.module.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ImageClassifierModule } from './image-classifier/image-classifier.module';
|
||||||
|
import { databaseConfig } from './config/database.config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ObjectDetectionModule } from './object-detection/object-detection.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forRoot(databaseConfig),
|
||||||
|
ImageClassifierModule,
|
||||||
|
ObjectDetectionModule,
|
||||||
|
],
|
||||||
|
controllers: [],
|
||||||
|
providers: [],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
11
microservices/src/config/database.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
export const databaseConfig: TypeOrmModuleOptions = {
|
||||||
|
type: 'postgres',
|
||||||
|
host: 'immich_postgres',
|
||||||
|
port: 5432,
|
||||||
|
username: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_DATABASE_NAME,
|
||||||
|
synchronize: false,
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
|
import { ImageClassifierService } from './image-classifier.service';
|
||||||
|
|
||||||
|
@Controller('image-classifier')
|
||||||
|
export class ImageClassifierController {
|
||||||
|
constructor(
|
||||||
|
private readonly imageClassifierService: ImageClassifierService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('/tagImage')
|
||||||
|
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
|
return await this.imageClassifierService.tagImage(thumbnailPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ImageClassifierService } from './image-classifier.service';
|
||||||
|
import { ImageClassifierController } from './image-classifier.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ImageClassifierController],
|
||||||
|
providers: [ImageClassifierService],
|
||||||
|
})
|
||||||
|
export class ImageClassifierModule {}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as mobilenet from '@tensorflow-models/mobilenet';
|
||||||
|
import * as cocoSsd from '@tensorflow-models/coco-ssd';
|
||||||
|
import * as tf from '@tensorflow/tfjs-node';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ImageClassifierService {
|
||||||
|
private readonly MOBILENET_VERSION = 2;
|
||||||
|
private readonly MOBILENET_ALPHA = 1.0;
|
||||||
|
|
||||||
|
private mobileNetModel: mobilenet.MobileNet;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
Logger.log(
|
||||||
|
`Running Node TensorFlow Version : ${tf.version['tfjs']}`,
|
||||||
|
'ImageClassifier',
|
||||||
|
);
|
||||||
|
mobilenet
|
||||||
|
.load({
|
||||||
|
version: this.MOBILENET_VERSION,
|
||||||
|
alpha: this.MOBILENET_ALPHA,
|
||||||
|
})
|
||||||
|
.then((mobilenetModel) => (this.mobileNetModel = mobilenetModel));
|
||||||
|
}
|
||||||
|
|
||||||
|
async tagImage(thumbnailPath: string) {
|
||||||
|
try {
|
||||||
|
const isExist = fs.existsSync(thumbnailPath);
|
||||||
|
if (isExist) {
|
||||||
|
const tags = [];
|
||||||
|
const image = fs.readFileSync(thumbnailPath);
|
||||||
|
const decodedImage = tf.node.decodeImage(image, 3) as tf.Tensor3D;
|
||||||
|
const predictions = await this.mobileNetModel.classify(decodedImage);
|
||||||
|
|
||||||
|
for (const prediction of predictions) {
|
||||||
|
if (prediction.probability >= 0.1) {
|
||||||
|
tags.push(...prediction.className.split(',').map((e) => e.trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tf.dispose(decodedImage);
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error reading file ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
microservices/src/main.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
await app.listen(3001, () => {
|
||||||
|
if (process.env.NODE_ENV == 'development') {
|
||||||
|
Logger.log(
|
||||||
|
'Running Immich Microservices in DEVELOPMENT environment',
|
||||||
|
'IMMICH MICROSERVICES',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV == 'production') {
|
||||||
|
Logger.log(
|
||||||
|
'Running Immich Microservices in PRODUCTION environment',
|
||||||
|
'IMMICH MICROSERVICES',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
|
import { ObjectDetectionService } from './object-detection.service';
|
||||||
|
|
||||||
|
@Controller('object-detection')
|
||||||
|
export class ObjectDetectionController {
|
||||||
|
constructor(
|
||||||
|
private readonly objectDetectionService: ObjectDetectionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('/detectObject')
|
||||||
|
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
|
return await this.objectDetectionService.detectObject(thumbnailPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ObjectDetectionService } from './object-detection.service';
|
||||||
|
import { ObjectDetectionController } from './object-detection.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ObjectDetectionController],
|
||||||
|
providers: [ObjectDetectionService],
|
||||||
|
})
|
||||||
|
export class ObjectDetectionModule {}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as cocoSsd from '@tensorflow-models/coco-ssd';
|
||||||
|
import * as tf from '@tensorflow/tfjs-node';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ObjectDetectionService {
|
||||||
|
private cocoSsdModel: cocoSsd.ObjectDetection;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
Logger.log(
|
||||||
|
`Running Node TensorFlow Version : ${tf.version['tfjs']}`,
|
||||||
|
'ObjectDetection',
|
||||||
|
);
|
||||||
|
cocoSsd.load().then((model) => (this.cocoSsdModel = model));
|
||||||
|
}
|
||||||
|
async detectObject(thumbnailPath: string) {
|
||||||
|
try {
|
||||||
|
const isExist = fs.existsSync(thumbnailPath);
|
||||||
|
if (isExist) {
|
||||||
|
const tags = new Set();
|
||||||
|
const image = fs.readFileSync(thumbnailPath);
|
||||||
|
const decodedImage = tf.node.decodeImage(image, 3) as tf.Tensor3D;
|
||||||
|
const predictions = await this.cocoSsdModel.detect(decodedImage);
|
||||||
|
|
||||||
|
for (const result of predictions) {
|
||||||
|
if (result.score > 0.5) {
|
||||||
|
tags.add(result.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tf.dispose(decodedImage);
|
||||||
|
return [...tags];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error reading file ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
microservices/test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AppModule } from '../src/app.module';
|
||||||
|
|
||||||
|
// End to End test
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
microservices/test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
microservices/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
21
microservices/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "app.alextran.immich"
|
applicationId "app.alextran.immich"
|
||||||
minSdkVersion 20
|
minSdkVersion 21
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@@ -20,4 +20,7 @@
|
|||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Added curated locations and objects on the search page
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* User can now download assets to local device
|
||||||
|
* Increased the font size for curated image thumbnail information on the seach page
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Added inline font, remove google-font dependency in pubspec.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
This is a client app for the self-hostable Immich Server (which can be found with the app's source repo). You will need to run/manage the server on your own in order to use the app.
|
||||||
|
|
||||||
|
Once set up, this app can be used as photo and video backup solution directly from your mobile phone.
|
||||||
|
|
||||||
|
<b>Features:</b>
|
||||||
|
|
||||||
|
* Upload and view assets(videos/images).
|
||||||
|
* Multi-user supported.
|
||||||
|
* Quick navigation with drag scroll bar.
|
||||||
|
* Auto Backup.
|
||||||
|
* 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)
|
||||||
|
* Upload assets from your local computer/server using <a href='https://www.npmjs.com/package/immich' target='_blank' rel='nofollow'>immich cli tools</a>
|
||||||
|
* [Optional] Reserve 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
|
||||||
|
After Width: | Height: | Size: 48 KiB |
BIN
mobile/android/fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 517 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1 @@
|
|||||||
|
This is a client app for the self-hostable Immich Server
|
||||||
1
mobile/android/fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Immich
|
||||||
@@ -4,3 +4,4 @@ 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-6.7-all.zip
|
||||||
|
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583
|
||||||
BIN
mobile/fonts/SnowburstOne.ttf
Normal file
BIN
mobile/fonts/WorkSans-Italic.ttf
Normal file
BIN
mobile/fonts/WorkSans.ttf
Normal file
@@ -13,7 +13,7 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- photo_manager (1.0.0):
|
- photo_manager (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
@@ -70,7 +70,7 @@ SPEC CHECKSUMS:
|
|||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
||||||
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
|
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
@@ -79,4 +79,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: 05c3056158482c567a3e0cdab1351ceeee238a07
|
PODFILE CHECKSUM: 05c3056158482c567a3e0cdab1351ceeee238a07
|
||||||
|
|
||||||
COCOAPODS: 1.10.1
|
COCOAPODS: 1.11.3
|
||||||
|
|||||||
@@ -341,7 +341,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
NEW_SETTING = "";
|
NEW_SETTING = "";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -425,7 +425,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
NEW_SETTING = "";
|
NEW_SETTING = "";
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@@ -475,7 +475,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
NEW_SETTING = "";
|
NEW_SETTING = "";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|||||||
@@ -1,66 +1,72 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Immich</string>
|
<string>Immich</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>immich_mobile</string>
|
<string>immich_mobile</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2</string>
|
<string>2</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true />
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
<string>Enable location setting to show position of assets on map</string>
|
<string>Enable location setting to show position of assets on map</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
|
||||||
<string>Enable location setting to show position of assets on map</string>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<string>Enable location setting to show position of assets on map</string>
|
||||||
<string>We need to manage backup your photos album</string>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>LaunchScreen</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<string>We need to manage backup your photos album</string>
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>LaunchScreen</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<key>UIMainStoryboardFile</key>
|
||||||
</array>
|
<string>Main</string>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
</array>
|
||||||
</array>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<key>UIUserInterfaceStyle</key>
|
<array>
|
||||||
<string>Light</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<true/>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<true/>
|
</array>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>UIUserInterfaceStyle</key>
|
||||||
<false/>
|
<string>Light</string>
|
||||||
</dict>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
</plist>
|
<true />
|
||||||
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
|
<true />
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false />
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -18,9 +18,12 @@ default_platform(:ios)
|
|||||||
platform :ios do
|
platform :ios do
|
||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_build_number({
|
increment_version_number(
|
||||||
build_number: latest_testflight_build_number + 1
|
version_number: "1.5.1"
|
||||||
})
|
)
|
||||||
|
increment_build_number(
|
||||||
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
)
|
||||||
build_app(scheme: "Runner",
|
build_app(scheme: "Runner",
|
||||||
workspace: "Runner.xcworkspace",
|
workspace: "Runner.xcworkspace",
|
||||||
xcargs: "-allowProvisioningUpdates")
|
xcargs: "-allowProvisioningUpdates")
|
||||||
@@ -29,19 +32,4 @@ platform :ios do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "iOS Release"
|
|
||||||
lane :release do
|
|
||||||
increment_version_number(
|
|
||||||
version_number: "1.3.0"
|
|
||||||
)
|
|
||||||
increment_build_number({
|
|
||||||
build_number: latest_testflight_build_number + 1
|
|
||||||
})
|
|
||||||
build_app(scheme: "Runner",
|
|
||||||
workspace: "Runner.xcworkspace",
|
|
||||||
xcargs: "-allowProvisioningUpdates")
|
|
||||||
upload_to_testflight(
|
|
||||||
skip_waiting_for_build_processing: true
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
|||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'constants/hive_box.dart';
|
import 'constants/hive_box.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
@@ -94,9 +93,11 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
primarySwatch: Colors.indigo,
|
primarySwatch: Colors.indigo,
|
||||||
textTheme: GoogleFonts.workSansTextTheme(
|
// textTheme: GoogleFonts.workSansTextTheme(
|
||||||
Theme.of(context).textTheme.apply(fontSizeFactor: 1.0),
|
// Theme.of(context).textTheme.apply(fontSizeFactor: 1.0),
|
||||||
),
|
// ),
|
||||||
|
fontFamily: 'WorkSans',
|
||||||
|
snackBarTheme: const SnackBarThemeData(contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
|
||||||
scaffoldBackgroundColor: const Color(0xFFf6f8fe),
|
scaffoldBackgroundColor: const Color(0xFFf6f8fe),
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
|
|||||||
@@ -1,28 +1,34 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
enum DownloadAssetStatus { idle, loading, success, error }
|
||||||
|
|
||||||
class ImageViewerPageState {
|
class ImageViewerPageState {
|
||||||
final bool isBottomSheetEnable;
|
// enum
|
||||||
|
final DownloadAssetStatus downloadAssetStatus;
|
||||||
|
|
||||||
ImageViewerPageState({
|
ImageViewerPageState({
|
||||||
required this.isBottomSheetEnable,
|
required this.downloadAssetStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
ImageViewerPageState copyWith({
|
ImageViewerPageState copyWith({
|
||||||
bool? isBottomSheetEnable,
|
DownloadAssetStatus? downloadAssetStatus,
|
||||||
}) {
|
}) {
|
||||||
return ImageViewerPageState(
|
return ImageViewerPageState(
|
||||||
isBottomSheetEnable: isBottomSheetEnable ?? this.isBottomSheetEnable,
|
downloadAssetStatus: downloadAssetStatus ?? this.downloadAssetStatus,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
final result = <String, dynamic>{};
|
||||||
'isBottomSheetEnable': isBottomSheetEnable,
|
|
||||||
};
|
result.addAll({'downloadAssetStatus': downloadAssetStatus.index});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
|
factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
|
||||||
return ImageViewerPageState(
|
return ImageViewerPageState(
|
||||||
isBottomSheetEnable: map['isBottomSheetEnable'] ?? false,
|
downloadAssetStatus: DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,15 +37,15 @@ class ImageViewerPageState {
|
|||||||
factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source));
|
factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ImageViewerPageState(isBottomSheetEnable: $isBottomSheetEnable)';
|
String toString() => 'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is ImageViewerPageState && other.isBottomSheetEnable == isBottomSheetEnable;
|
return other is ImageViewerPageState && other.downloadAssetStatus == downloadAssetStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => isBottomSheetEnable.hashCode;
|
int get hashCode => downloadAssetStatus.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
class RequestDownloadAssetInfo {
|
||||||
|
final String assetId;
|
||||||
|
final String deviceId;
|
||||||
|
|
||||||
|
RequestDownloadAssetInfo(this.assetId, this.deviceId);
|
||||||
|
}
|
||||||
@@ -1,21 +1,43 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
|
||||||
class ImageViewerPageStateNotifier extends StateNotifier<ImageViewerPageState> {
|
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
ImageViewerPageStateNotifier() : super(ImageViewerPageState(isBottomSheetEnable: false));
|
final ImageViewerService _imageViewerService = ImageViewerService();
|
||||||
|
|
||||||
void toggleBottomSheet() {
|
ImageViewerStateNotifier() : super(ImageViewerPageState(downloadAssetStatus: DownloadAssetStatus.idle));
|
||||||
bool isBottomSheetEnable = state.isBottomSheetEnable;
|
|
||||||
|
|
||||||
if (isBottomSheetEnable) {
|
void downloadAsset(ImmichAsset asset, BuildContext context) async {
|
||||||
state.copyWith(isBottomSheetEnable: false);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
||||||
|
|
||||||
|
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
|
||||||
|
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Download Success",
|
||||||
|
toastType: ToastType.success,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
state.copyWith(isBottomSheetEnable: true);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Download Error",
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final homePageStateProvider = StateNotifierProvider<ImageViewerPageStateNotifier, ImageViewerPageState>(
|
final imageViewerStateProvider =
|
||||||
((ref) => ImageViewerPageStateNotifier()));
|
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(((ref) => ImageViewerStateNotifier()));
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
class ImageViewerService {
|
||||||
|
Future<bool> downloadAssetToDevice(ImmichAsset asset) async {
|
||||||
|
try {
|
||||||
|
String fileName = p.basename(asset.originalPath);
|
||||||
|
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
|
Uri filePath =
|
||||||
|
Uri.parse("$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
|
||||||
|
|
||||||
|
var res = await http.get(
|
||||||
|
filePath,
|
||||||
|
headers: {"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"},
|
||||||
|
);
|
||||||
|
|
||||||
|
final AssetEntity? entity;
|
||||||
|
|
||||||
|
if (asset.type == 'IMAGE') {
|
||||||
|
entity = await PhotoManager.editor.saveImage(
|
||||||
|
res.bodyBytes,
|
||||||
|
title: p.basename(asset.originalPath),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
File tempFile = await File('${tempDir.path}/$fileName').create();
|
||||||
|
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||||
|
entity = await PhotoManager.editor.saveVideo(tempFile, title: fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error saving file $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
|
|
||||||
|
class DownloadLoadingIndicator extends StatelessWidget {
|
||||||
|
const DownloadLoadingIndicator({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 60,
|
||||||
|
width: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const SpinKitDancingSquare(
|
||||||
|
color: Colors.white,
|
||||||
|
size: 30.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
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:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
|
||||||
class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
|
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
const TopControlAppBar({Key? key, required this.asset, required this.onMoreInfoPressed}) : super(key: key);
|
const TopControlAppBar(
|
||||||
|
{Key? key, required this.asset, required this.onMoreInfoPressed, required this.onDownloadPressed})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
final ImmichAsset asset;
|
final ImmichAsset asset;
|
||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
|
final Function onDownloadPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
double iconSize = 18.0;
|
double iconSize = 18.0;
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
@@ -29,7 +34,7 @@ class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
|
|||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
print("download");
|
onDownloadPressed();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.cloud_download_rounded),
|
icon: const Icon(Icons.cloud_download_rounded),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/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/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';
|
||||||
@@ -25,6 +28,7 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
@@ -42,65 +46,77 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
asset: asset,
|
asset: asset,
|
||||||
onMoreInfoPressed: () {
|
onMoreInfoPressed: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
barrierColor: Colors.transparent,
|
barrierColor: Colors.transparent,
|
||||||
isScrollControlled: false,
|
isScrollControlled: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ExifBottomSheet(assetDetail: assetDetail!);
|
return ExifBottomSheet(assetDetail: assetDetail!);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onDownloadPressed: () {
|
||||||
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Center(
|
child: Stack(
|
||||||
child: Hero(
|
children: [
|
||||||
tag: heroTag,
|
Center(
|
||||||
child: CachedNetworkImage(
|
child: Hero(
|
||||||
fit: BoxFit.cover,
|
tag: heroTag,
|
||||||
imageUrl: imageUrl,
|
child: CachedNetworkImage(
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
fit: BoxFit.cover,
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
imageUrl: imageUrl,
|
||||||
errorWidget: (context, url, error) => ConstrainedBox(
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
fadeInDuration: const Duration(milliseconds: 250),
|
||||||
child: Wrap(
|
errorWidget: (context, url, error) => ConstrainedBox(
|
||||||
spacing: 32,
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
runSpacing: 32,
|
child: Wrap(
|
||||||
alignment: WrapAlignment.center,
|
spacing: 32,
|
||||||
children: [
|
runSpacing: 32,
|
||||||
const Text(
|
alignment: WrapAlignment.center,
|
||||||
"Failed To Render Image - Possibly Corrupted Data",
|
children: [
|
||||||
textAlign: TextAlign.center,
|
const Text(
|
||||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
"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]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SingleChildScrollView(
|
),
|
||||||
child: Text(
|
placeholder: (context, url) {
|
||||||
error.toString(),
|
return CachedNetworkImage(
|
||||||
textAlign: TextAlign.center,
|
cacheKey: thumbnailUrl,
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
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],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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)
|
||||||
|
const Center(
|
||||||
|
child: DownloadLoadingIndicator(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,35 +1,74 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/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/exif_bottom_sheet.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
class VideoViewerPage extends StatelessWidget {
|
// ignore: must_be_immutable
|
||||||
|
class VideoViewerPage extends HookConsumerWidget {
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
|
final ImmichAsset asset;
|
||||||
|
ImmichAssetWithExif? assetDetail;
|
||||||
|
final AssetService _assetService = AssetService();
|
||||||
|
|
||||||
const VideoViewerPage({Key? key, required this.videoUrl}) : super(key: key);
|
VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
|
|
||||||
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
|
||||||
|
getAssetExif() async {
|
||||||
|
assetDetail = await _assetService.getAssetById(asset.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
getAssetExif();
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
appBar: AppBar(
|
appBar: TopControlAppBar(
|
||||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
asset: asset,
|
||||||
backgroundColor: Colors.black,
|
onMoreInfoPressed: () {
|
||||||
leading: IconButton(
|
showModalBottomSheet(
|
||||||
onPressed: () {
|
backgroundColor: Colors.black,
|
||||||
AutoRouter.of(context).pop();
|
barrierColor: Colors.transparent,
|
||||||
|
isScrollControlled: false,
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return ExifBottomSheet(assetDetail: assetDetail!);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back_ios)),
|
);
|
||||||
|
},
|
||||||
|
onDownloadPressed: () {
|
||||||
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: VideoThumbnailPlayer(
|
child: Stack(
|
||||||
url: videoUrl,
|
children: [
|
||||||
jwtToken: jwtToken,
|
VideoThumbnailPlayer(
|
||||||
|
url: videoUrl,
|
||||||
|
jwtToken: jwtToken,
|
||||||
|
),
|
||||||
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
|
const Center(
|
||||||
|
child: DownloadLoadingIndicator(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:badges/badges.dart';
|
import 'package:badges/badges.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
|
||||||
@@ -79,12 +78,11 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'IMMICH',
|
'IMMICH',
|
||||||
style: GoogleFonts.snowburstOne(
|
style: TextStyle(
|
||||||
textStyle: TextStyle(
|
fontFamily: 'SnowburstOne',
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -121,8 +119,12 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: const Icon(Icons.backup_rounded)),
|
child: const Icon(Icons.backup_rounded)),
|
||||||
tooltip: 'Backup Controller',
|
tooltip: 'Backup Controller',
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
AutoRouter.of(context).push(const BackupControllerRoute());
|
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
|
||||||
|
|
||||||
|
if (onPop != null && onPop == true) {
|
||||||
|
onPopBack!();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_backupState.backupProgress == BackUpProgressEnum.inProgress
|
_backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
VideoViewerRoute(
|
VideoViewerRoute(
|
||||||
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
||||||
),
|
asset: asset),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ class HomePage extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
void reloadAllAsset() {
|
||||||
|
ref.read(assetProvider.notifier).getAllAsset();
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
if (assetGroupByDateTime.isNotEmpty) {
|
if (assetGroupByDateTime.isNotEmpty) {
|
||||||
int? lastMonth;
|
int? lastMonth;
|
||||||
@@ -86,7 +90,9 @@ class HomePage extends HookConsumerWidget {
|
|||||||
child: null,
|
child: null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const ImmichSliverAppBar(),
|
: ImmichSliverAppBar(
|
||||||
|
onPopBack: reloadAllAsset,
|
||||||
|
),
|
||||||
duration: const Duration(milliseconds: 350),
|
duration: const Duration(milliseconds: 350),
|
||||||
),
|
),
|
||||||
..._imageGridGroup
|
..._imageGridGroup
|
||||||
|
|||||||
@@ -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:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
@@ -15,7 +14,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final usernameController = useTextEditingController(text: 'testuser@email.com');
|
final usernameController = useTextEditingController(text: 'testuser@email.com');
|
||||||
final passwordController = useTextEditingController(text: 'password');
|
final passwordController = useTextEditingController(text: 'password');
|
||||||
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:2283');
|
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216:2283');
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
@@ -33,9 +32,12 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'IMMICH',
|
'IMMICH',
|
||||||
style: GoogleFonts.snowburstOne(
|
style: TextStyle(
|
||||||
textStyle:
|
fontFamily: 'SnowburstOne',
|
||||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 48, color: Theme.of(context).primaryColor)),
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 48,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
EmailInput(controller: usernameController),
|
EmailInput(controller: usernameController),
|
||||||
PasswordInput(controller: passwordController),
|
PasswordInput(controller: passwordController),
|
||||||
@@ -78,7 +80,7 @@ class EmailInput extends StatelessWidget {
|
|||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration:
|
decoration:
|
||||||
const InputDecoration(labelText: 'email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
|
const InputDecoration(labelText: 'Email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,9 +130,10 @@ class LoginButton extends ConsumerWidget {
|
|||||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Error logging you in, check server url, email and password!",
|
msg: "Error logging you in, check server url, email and password!",
|
||||||
toastType: ToastType.error);
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text("Login"));
|
child: const Text("Login"));
|
||||||
|
|||||||
80
mobile/lib/modules/search/models/curated_object.model.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class CuratedObject {
|
||||||
|
final String id;
|
||||||
|
final String object;
|
||||||
|
final String resizePath;
|
||||||
|
final String deviceAssetId;
|
||||||
|
final String deviceId;
|
||||||
|
CuratedObject({
|
||||||
|
required this.id,
|
||||||
|
required this.object,
|
||||||
|
required this.resizePath,
|
||||||
|
required this.deviceAssetId,
|
||||||
|
required this.deviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
CuratedObject copyWith({
|
||||||
|
String? id,
|
||||||
|
String? object,
|
||||||
|
String? resizePath,
|
||||||
|
String? deviceAssetId,
|
||||||
|
String? deviceId,
|
||||||
|
}) {
|
||||||
|
return CuratedObject(
|
||||||
|
id: id ?? this.id,
|
||||||
|
object: object ?? this.object,
|
||||||
|
resizePath: resizePath ?? this.resizePath,
|
||||||
|
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
||||||
|
deviceId: deviceId ?? this.deviceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'id': id});
|
||||||
|
result.addAll({'object': object});
|
||||||
|
result.addAll({'resizePath': resizePath});
|
||||||
|
result.addAll({'deviceAssetId': deviceAssetId});
|
||||||
|
result.addAll({'deviceId': deviceId});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CuratedObject.fromMap(Map<String, dynamic> map) {
|
||||||
|
return CuratedObject(
|
||||||
|
id: map['id'] ?? '',
|
||||||
|
object: map['object'] ?? '',
|
||||||
|
resizePath: map['resizePath'] ?? '',
|
||||||
|
deviceAssetId: map['deviceAssetId'] ?? '',
|
||||||
|
deviceId: map['deviceId'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory CuratedObject.fromJson(String source) => CuratedObject.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CuratedObject(id: $id, object: $object, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is CuratedObject &&
|
||||||
|
other.id == id &&
|
||||||
|
other.object == object &&
|
||||||
|
other.resizePath == resizePath &&
|
||||||
|
other.deviceAssetId == deviceAssetId &&
|
||||||
|
other.deviceId == deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^ object.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
|
import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
@@ -64,3 +65,14 @@ final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocati
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final getCuratedObjectProvider = FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
|
||||||
|
final SearchService _searchService = SearchService();
|
||||||
|
|
||||||
|
var curatedObject = await _searchService.getCuratedObjects();
|
||||||
|
if (curatedObject != null) {
|
||||||
|
return curatedObject;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
|
|
||||||
@@ -52,4 +53,19 @@ class SearchService {
|
|||||||
throw Error();
|
throw Error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<CuratedObject>?> getCuratedObjects() async {
|
||||||
|
try {
|
||||||
|
var res = await _networkService.getRequest(url: "asset/allObjects");
|
||||||
|
|
||||||
|
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||||
|
|
||||||
|
List<CuratedObject> result = List.from(decodedData.map((a) => CuratedObject.fromMap(a)));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("[ERROR] [CuratedObject] ${e.toString()}");
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
mobile/lib/modules/search/ui/thumbnail_with_info.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
||||||
|
|
||||||
|
class ThumbnailWithInfo extends StatelessWidget {
|
||||||
|
const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
final String textInfo;
|
||||||
|
final String imageUrl;
|
||||||
|
final Function onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
onTap();
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width / 2,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
color: Colors.black26,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 8,
|
||||||
|
left: 10,
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width / 3,
|
||||||
|
child: Text(
|
||||||
|
textInfo.capitalizeFirstLetter(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
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: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/search/models/curated_location.model.dart';
|
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class SearchPage extends HookConsumerWidget {
|
class SearchPage extends HookConsumerWidget {
|
||||||
@@ -22,6 +24,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
||||||
AsyncValue<List<CuratedLocation>> curatedLocation = ref.watch(getCuratedLocationProvider);
|
AsyncValue<List<CuratedLocation>> curatedLocation = ref.watch(getCuratedLocationProvider);
|
||||||
|
AsyncValue<List<CuratedObject>> curatedObjects = ref.watch(getCuratedObjectProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
searchFocusNode = FocusNode();
|
searchFocusNode = FocusNode();
|
||||||
@@ -37,12 +40,12 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
_buildPlaces() {
|
_buildPlaces() {
|
||||||
return curatedLocation.when(
|
return curatedLocation.when(
|
||||||
loading: () => const CircularProgressIndicator(),
|
loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()),
|
||||||
error: (err, stack) => Text('Error: $err'),
|
error: (err, stack) => Text('Error: $err'),
|
||||||
data: (curatedLocations) {
|
data: (curatedLocations) {
|
||||||
return curatedLocations.isNotEmpty
|
return curatedLocations.isNotEmpty
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
height: MediaQuery.of(context).size.width / 3,
|
height: MediaQuery.of(context).size.width / 2,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.only(left: 16),
|
padding: const EdgeInsets.only(left: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
@@ -63,7 +66,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: SizedBox(
|
: SizedBox(
|
||||||
height: MediaQuery.of(context).size.width / 3,
|
height: MediaQuery.of(context).size.width / 2,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.only(left: 16),
|
padding: const EdgeInsets.only(left: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
@@ -82,6 +85,54 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildThings() {
|
||||||
|
return curatedObjects.when(
|
||||||
|
loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()),
|
||||||
|
error: (err, stack) => Text('Error: $err'),
|
||||||
|
data: (objects) {
|
||||||
|
return objects.isNotEmpty
|
||||||
|
? SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.width / 2,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(left: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: curatedObjects.value?.length,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
CuratedObject curatedObjectInfo = objects[index];
|
||||||
|
var thumbnailRequestUrl =
|
||||||
|
'${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true';
|
||||||
|
|
||||||
|
return ThumbnailWithInfo(
|
||||||
|
imageUrl: thumbnailRequestUrl,
|
||||||
|
textInfo: curatedObjectInfo.object,
|
||||||
|
onTap: () {
|
||||||
|
AutoRouter.of(context)
|
||||||
|
.push(SearchResultRoute(searchTerm: curatedObjectInfo.object.capitalizeFirstLetter()));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.width / 2,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(left: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: 1,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
return ThumbnailWithInfo(
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
|
||||||
|
textInfo: 'No Object Info Available',
|
||||||
|
onTap: () {},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: SearchBar(
|
appBar: SearchBar(
|
||||||
searchFocusNode: searchFocusNode,
|
searchFocusNode: searchFocusNode,
|
||||||
@@ -104,6 +155,14 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildPlaces(),
|
_buildPlaces(),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
"Things",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildThings()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
|
isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
|
||||||
@@ -113,66 +172,3 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThumbnailWithInfo extends StatelessWidget {
|
|
||||||
const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
final String textInfo;
|
|
||||||
final String imageUrl;
|
|
||||||
final Function onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
onTap();
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
|
||||||
child: SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width / 3,
|
|
||||||
height: MediaQuery.of(context).size.width / 3,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
foregroundDecoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
color: Colors.black26,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
width: 150,
|
|
||||||
height: 150,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 8,
|
|
||||||
left: 10,
|
|
||||||
child: SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width / 3,
|
|
||||||
child: Text(
|
|
||||||
textInfo,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
|
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
||||||
@@ -107,7 +108,10 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchResultPageState.isLoading) {
|
if (searchResultPageState.isLoading) {
|
||||||
return const CircularProgressIndicator.adaptive();
|
return Center(
|
||||||
|
child: SpinKitDancingSquare(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchResultPageState.isSuccess) {
|
if (searchResultPageState.isSuccess) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|||||||
import 'package:immich_mobile/shared/views/backup_controller_page.dart';
|
import 'package:immich_mobile/shared/views/backup_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
||||||
import 'package:immich_mobile/shared/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
|
|
||||||
part 'router.gr.dart';
|
part 'router.gr.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
final args = routeData.argsAs<VideoViewerRouteArgs>();
|
final args = routeData.argsAs<VideoViewerRouteArgs>();
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl));
|
child: VideoViewerPage(
|
||||||
|
key: args.key, videoUrl: args.videoUrl, asset: args.asset));
|
||||||
},
|
},
|
||||||
BackupControllerRoute.name: (routeData) {
|
BackupControllerRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
@@ -163,24 +164,29 @@ class ImageViewerRouteArgs {
|
|||||||
/// generated route for
|
/// generated route for
|
||||||
/// [VideoViewerPage]
|
/// [VideoViewerPage]
|
||||||
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||||
VideoViewerRoute({Key? key, required String videoUrl})
|
VideoViewerRoute(
|
||||||
|
{Key? key, required String videoUrl, required ImmichAsset asset})
|
||||||
: super(VideoViewerRoute.name,
|
: super(VideoViewerRoute.name,
|
||||||
path: '/video-viewer-page',
|
path: '/video-viewer-page',
|
||||||
args: VideoViewerRouteArgs(key: key, videoUrl: videoUrl));
|
args: VideoViewerRouteArgs(
|
||||||
|
key: key, videoUrl: videoUrl, asset: asset));
|
||||||
|
|
||||||
static const String name = 'VideoViewerRoute';
|
static const String name = 'VideoViewerRoute';
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoViewerRouteArgs {
|
class VideoViewerRouteArgs {
|
||||||
const VideoViewerRouteArgs({this.key, required this.videoUrl});
|
const VideoViewerRouteArgs(
|
||||||
|
{this.key, required this.videoUrl, required this.asset});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
|
|
||||||
|
final ImmichAsset asset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}';
|
return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl, asset: $asset}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||||||
if (route.name == 'SearchRoute') {
|
if (route.name == 'SearchRoute') {
|
||||||
// Refresh Location State
|
// Refresh Location State
|
||||||
ref.refresh(getCuratedLocationProvider);
|
ref.refresh(getCuratedLocationProvider);
|
||||||
|
ref.refresh(getCuratedObjectProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class ImmichAsset {
|
|||||||
final String modifiedAt;
|
final String modifiedAt;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
final String? duration;
|
final String? duration;
|
||||||
|
final String originalPath;
|
||||||
|
final String resizePath;
|
||||||
|
|
||||||
ImmichAsset({
|
ImmichAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -21,6 +23,8 @@ class ImmichAsset {
|
|||||||
required this.modifiedAt,
|
required this.modifiedAt,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
this.duration,
|
this.duration,
|
||||||
|
required this.originalPath,
|
||||||
|
required this.resizePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
ImmichAsset copyWith({
|
ImmichAsset copyWith({
|
||||||
@@ -33,6 +37,8 @@ class ImmichAsset {
|
|||||||
String? modifiedAt,
|
String? modifiedAt,
|
||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
String? duration,
|
String? duration,
|
||||||
|
String? originalPath,
|
||||||
|
String? resizePath,
|
||||||
}) {
|
}) {
|
||||||
return ImmichAsset(
|
return ImmichAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -44,6 +50,8 @@ class ImmichAsset {
|
|||||||
modifiedAt: modifiedAt ?? this.modifiedAt,
|
modifiedAt: modifiedAt ?? this.modifiedAt,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
duration: duration ?? this.duration,
|
duration: duration ?? this.duration,
|
||||||
|
originalPath: originalPath ?? this.originalPath,
|
||||||
|
resizePath: resizePath ?? this.resizePath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +66,8 @@ class ImmichAsset {
|
|||||||
'modifiedAt': modifiedAt,
|
'modifiedAt': modifiedAt,
|
||||||
'isFavorite': isFavorite,
|
'isFavorite': isFavorite,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
|
'originalPath': originalPath,
|
||||||
|
'resizePath': resizePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +82,8 @@ class ImmichAsset {
|
|||||||
modifiedAt: map['modifiedAt'] ?? '',
|
modifiedAt: map['modifiedAt'] ?? '',
|
||||||
isFavorite: map['isFavorite'] ?? false,
|
isFavorite: map['isFavorite'] ?? false,
|
||||||
duration: map['duration'],
|
duration: map['duration'],
|
||||||
|
originalPath: map['originalPath'] ?? '',
|
||||||
|
resizePath: map['resizePath'] ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +93,7 @@ class ImmichAsset {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration)';
|
return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration, originalPath: $originalPath, resizePath: $resizePath)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -97,7 +109,9 @@ class ImmichAsset {
|
|||||||
other.createdAt == createdAt &&
|
other.createdAt == createdAt &&
|
||||||
other.modifiedAt == modifiedAt &&
|
other.modifiedAt == modifiedAt &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.duration == duration;
|
other.duration == duration &&
|
||||||
|
other.originalPath == originalPath &&
|
||||||
|
other.resizePath == resizePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -110,6 +124,8 @@ class ImmichAsset {
|
|||||||
createdAt.hashCode ^
|
createdAt.hashCode ^
|
||||||
modifiedAt.hashCode ^
|
modifiedAt.hashCode ^
|
||||||
isFavorite.hashCode ^
|
isFavorite.hashCode ^
|
||||||
duration.hashCode;
|
duration.hashCode ^
|
||||||
|
originalPath.hashCode ^
|
||||||
|
resizePath.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,20 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopListenToEvent(String eventName) {
|
||||||
|
debugPrint("[Websocket] Stop listening to event $eventName");
|
||||||
|
state.socket?.off(eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
listenUploadEvent() {
|
||||||
|
debugPrint("[Websocket] Start listening to event on_upload_success");
|
||||||
|
state.socket?.on('on_upload_success', (data) {
|
||||||
|
var jsonString = jsonDecode(data.toString());
|
||||||
|
ImmichAsset newAsset = ImmichAsset.fromMap(jsonString);
|
||||||
|
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
|
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class BackupService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Build thumbnail multipart data
|
// Build thumbnail multipart data
|
||||||
var thumbnailData = await entity.thumbDataWithSize(1280, 720);
|
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280));
|
||||||
if (thumbnailData != null) {
|
if (thumbnailData != null) {
|
||||||
thumbnailUploadData = MultipartFile.fromBytes(
|
thumbnailUploadData = MultipartFile.fromBytes(
|
||||||
List.from(thumbnailData),
|
List.from(thumbnailData),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -25,16 +26,36 @@ class NetworkService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> getRequest({required String url}) async {
|
Future<dynamic> getRequest({required String url, bool isByteResponse = false, bool isStreamReponse = false}) async {
|
||||||
try {
|
try {
|
||||||
var dio = Dio();
|
var dio = Dio();
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
||||||
|
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
Response res = await dio.get('$savedEndpoint/$url');
|
|
||||||
|
|
||||||
if (res.statusCode == 200) {
|
if (isByteResponse) {
|
||||||
return res;
|
Response<List<int>> res = await dio.get<List<int>>(
|
||||||
|
'$savedEndpoint/$url',
|
||||||
|
options: Options(responseType: ResponseType.bytes),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} else if (isStreamReponse) {
|
||||||
|
Response<ResponseBody> res = await dio.get<ResponseBody>(
|
||||||
|
'$savedEndpoint/$url',
|
||||||
|
options: Options(responseType: ResponseType.stream),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Response res = await dio.get('$savedEndpoint/$url');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} on DioError catch (e) {
|
} on DioError catch (e) {
|
||||||
debugPrint("DioError: ${e.response}");
|
debugPrint("DioError: ${e.response}");
|
||||||
|
|||||||
@@ -8,12 +8,24 @@ class ImmichToast {
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String msg,
|
required String msg,
|
||||||
ToastType toastType = ToastType.info,
|
ToastType toastType = ToastType.info,
|
||||||
|
ToastGravity gravity = ToastGravity.TOP,
|
||||||
}) {
|
}) {
|
||||||
FToast fToast;
|
FToast fToast;
|
||||||
|
|
||||||
fToast = FToast();
|
fToast = FToast();
|
||||||
fToast.init(context);
|
fToast.init(context);
|
||||||
|
|
||||||
|
_getColor(ToastType type, BuildContext context) {
|
||||||
|
switch (type) {
|
||||||
|
case ToastType.info:
|
||||||
|
return Theme.of(context).primaryColor;
|
||||||
|
case ToastType.success:
|
||||||
|
return const Color.fromARGB(255, 78, 140, 124);
|
||||||
|
case ToastType.error:
|
||||||
|
return const Color.fromARGB(255, 220, 48, 85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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),
|
||||||
@@ -36,8 +48,8 @@ class ImmichToast {
|
|||||||
: Container(),
|
: Container(),
|
||||||
(toastType == ToastType.success)
|
(toastType == ToastType.success)
|
||||||
? const Icon(
|
? const Icon(
|
||||||
Icons.check,
|
Icons.check_circle_rounded,
|
||||||
color: Color.fromARGB(255, 104, 248, 140),
|
color: Color.fromARGB(255, 78, 140, 124),
|
||||||
)
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
(toastType == ToastType.error)
|
(toastType == ToastType.error)
|
||||||
@@ -53,7 +65,7 @@ class ImmichToast {
|
|||||||
child: Text(
|
child: Text(
|
||||||
msg,
|
msg,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: _getColor(toastType, context),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
),
|
),
|
||||||
@@ -62,7 +74,7 @@ class ImmichToast {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
gravity: ToastGravity.TOP,
|
gravity: gravity,
|
||||||
toastDuration: const Duration(seconds: 2),
|
toastDuration: const Duration(seconds: 2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:immich_mobile/modules/login/models/authentication_state.model.da
|
|||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
|
|
||||||
class BackupControllerPage extends HookConsumerWidget {
|
class BackupControllerPage extends HookConsumerWidget {
|
||||||
@@ -23,6 +24,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
ref.read(backupProvider.notifier).getBackupInfo();
|
ref.read(backupProvider.notifier).getBackupInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success');
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -107,6 +109,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
||||||
AutoRouter.of(context).pop(true);
|
AutoRouter.of(context).pop(true);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded)),
|
icon: const Icon(Icons.arrow_back_ios_rounded)),
|
||||||
@@ -125,7 +128,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Total",
|
title: "Total",
|
||||||
subtitle: "All images and video on the device",
|
subtitle: "All images and videos on the device",
|
||||||
info: "${_backupState.totalAssetCount}",
|
info: "${_backupState.totalAssetCount}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
|
|||||||