Compare commits

...

42 Commits

Author SHA1 Message Date
Alex
a8220172f8 WIP refactor container and queuing system (#206)
* refactor microservices to machine-learning

* Update tGithub issue template with correct task syntax

* Added microservices container

* Communicate between service based on queue system

* added dependency

* Fixed problem with having to import BullQueue into the individual service

* Added todo

* refactor server into monorepo with microservices

* refactor database and entity to library

* added simple migration

* Move migrations and database config to library

* Migration works in library

* Cosmetic change in logging message

* added user dto

* Fixed issue with testing not able to find the shared library

* Clean up library mapping path

* Added webp generator to microservices

* Update Github Action build latest

* Fixed issue NPM cannot install due to conflict witl Bull Queue

* format project with prettier

* Modified docker-compose file

* Add GH Action for Staging build:

* Fixed GH action job name

* Modified GH Action to only build & push latest when pushing to main

* Added Test 2e2 Github Action

* Added Test 2e2 Github Action

* Implemented microservice to extract exif

* Added cronjob to scan and generate webp thumbnail  at midnight

* Refactor to ireduce hit time to database when running microservices

* Added error handling to asset services that handle read file from disk

* Added video transcoding queue to process one video at a time

* Fixed loading spinner on web while loading covering the info panel

* Add mechanism to show new release announcement to web and mobile app (#209)

* Added changelog page

* Fixed issues based on PR comments

* Fixed issue with video transcoding run on the server

* Change entry point content for backward combatibility when starting up server

* Added announcement box

* Added error handling to failed silently when the app version checking is not able to make the request to GITHUB

* Added new version announcement overlay

* Update message

* Added messages

* Added logic to check and show announcement

* Add method to handle saving new version

* Added button to dimiss the acknowledge message

* Up version for deployment to the app store
2022-06-11 16:12:06 -05:00
Alex
397f8c70b4 Fixed NPM build dependency conflict for server 2022-06-07 08:08:22 -05:00
Matthias Rupp
68ff5377b0 Minor improvements to the detail-panel component (#205)
* Fix roudning behavior in details panel

* Add lat,lon-popup to map in details

* Refactor map code in detail-panel to avoid duplicate code
2022-06-06 16:40:12 -05:00
Jaime Baez
b359dc3cb6 Fix user e2e tests (#194)
* WIP fix user e2e tests

The e2e tests still don't seem to work due to migrations not running.

Changes:
- update user.e2e tests to use new `userService.createUser` method
- fix server `typeorm` command to use ORM config
- update make test-e2e to re-create database volume every time
- add User DTO
- update auth.service and user.service to use User DTO
- update CreateUserDto making optional properties that are optional

* Fix migrations
- add missing `.ts` extension to migrations path
- update user e2e test for the new returned User resource
2022-06-06 11:16:03 -05:00
Zack Pollard
5b036067ed Fix sidebar layout (#204)
* fix: sidebar margins with more than one item incorrect

* fix: api url in sidebar shouldn't overflow the sidebar width
2022-06-05 21:12:12 -05:00
Alex
b9f38162d5 Implemented status box on the side bar (#201) 2022-06-05 05:15:39 -05:00
Alex
ab6909bfbd 20 video conversion for web view (#200)
* Added job for video conversion every 1 minute

* Handle get video as mp4 on the web

* Auto play video on web on hovered

* Added video player

* Added animation and video duration to thumbnail player

* Fixed issue with video not playing on hover

* Added animation when loading thumbnail
2022-06-04 18:34:11 -05:00
Alex
53c3c916a6 View assets detail and download operation (#198)
* Fixed not displaying default user profile picture

* Added buttons to close viewer and micro-interaction for navigating assets left, right

* Add additional buttons to the control bar

* Display EXIF info

* Added map to detail info

* Handle user input keyboard

* Fixed incorrect file name when downloading multiple files

* Implemented download panel
2022-06-03 11:04:30 -05:00
Alex
6924aa5eb1 Update font size of 'create shared album' button to keep the text on one line 2022-05-29 18:15:16 -05:00
Alex
a3b45d62b6 175 Fixed issue back button android return to login page (#193)
* Back button is no longer return to login page

* Update to material 3

* Update to material 3

* Up version for deployment

* Added F-droid changelog
2022-05-29 17:32:30 -05:00
Alex Tran
b34de624ce Added changelog for F-droid 2022-05-29 09:11:22 -05:00
Alex Tran
7886c42742 Update Fastlane iOS build version + speicify database container to restart always 2022-05-29 08:42:27 -05:00
Alex
d476b15312 Implemented user profile upload and show on web/mobile (#191)
* Update mobile dependencies

* Added image picker

* Added mechanism to upload profile image

* Added image type to send to web

* Added styling for circle avatar

* Fixxed issue with sharp cannot resize image properly

* Finished displaying and uploading user profile

* Added user profile to web
2022-05-28 22:35:45 -05:00
Alex Tran
bdf38e7668 Added endpoint for getting user profile image 2022-05-27 22:24:58 -05:00
Alex Tran
e33566a04a Upload profile picture and convert into webp 2022-05-27 22:15:35 -05:00
Alex
c28251b8b4 [WEB] View large images on web (#189)
* Added selection icon to thumbnail

* Added micro-interaction and video file indication

* Added page to add page

* Added image viewer

* navigate assets

* Added separate component for viewing the video file

* Added FFmpeg modules

* Added correct content-type header for serving image file

* Added loading spinner
2022-05-27 14:02:06 -05:00
Alex Tran
337db1c508 fixed typo for bug template 2022-05-26 15:12:08 -05:00
Alex Tran
ad2a1ba901 Update and add issue templates 2022-05-26 15:10:50 -05:00
Alex Tran
fa6f6f8e9f Fixed default DB_HOSTNAME to be the correct database container 2022-05-24 08:49:38 -05:00
Pavle Portic
a44043a4e5 Add ability to pass redis hostname as env var (#174)
* Add ability to pass redis hostname as env var

* Read postgres host from env var in microservices

* Update .env.example with postgres and redis hostname vars
2022-05-23 17:23:02 -05:00
shruuub
87b15c60c0 Fix typos and grammar, plus change Linux to Unix (#180)
Fixed Readme typo
2022-05-23 11:58:00 -05:00
Alex
2c83e52c15 Added dependency in docker-compose (#173)
* Added dependency in docker-compose
* downgrade sharp version to 0.28
2022-05-22 10:15:38 -05:00
Alex
55c5027539 Add webp thumbnail conversion task to optimize performance of fast scrolling (#172)
* Update readme

* Added webp to table and entity

* Added cronjob and sharp dependencies

* Added conversion of webp every 5 minutes and endpoint will now server webp image if exist
2022-05-22 06:56:36 -05:00
Alex
ce06af0c9b Fixed lodash library not invoking in production build (#171)
* Added staging docker-compose file
* Use lodash-es and remove hydration option on photos page fixed the problem
2022-05-22 04:48:38 -05:00
Alex Tran
baaf7ad153 disable hydration 2022-05-21 23:50:54 -05:00
Alex Tran
4a25e7dc22 remove footer to fix error when reloading the page 2022-05-21 23:36:58 -05:00
Alex Tran
f90563d18c not prerender photos page 2022-05-21 23:28:02 -05:00
Alex Tran
8352ecd3b9 Update readme 2022-05-21 18:53:30 -05:00
Alex Tran
69b34a4364 Update readme 2022-05-21 18:49:23 -05:00
Alex
6023c3c624 Show assets on web (#168)
* Implemented lazy loading thumbnail
* Display assets as date-time grouping
* Update Readme
* Modify GitHub action to run from the latest update
2022-05-21 16:50:56 -05:00
Alex Tran
171e7ffa77 Update readme 2022-05-21 08:30:27 -05:00
Alex Tran
d9f918005a Added python3 to prod target of web Dockerfile 2022-05-21 02:34:39 -05:00
Alex Tran
e8ade4866b Added python3 to docker image of web 2022-05-21 02:30:00 -05:00
Alex Tran
bbfa789a4e update readme 2022-05-21 02:25:15 -05:00
Alex
a779c3803c Add web interface with admin functionality (#167) 2022-05-21 02:23:55 -05:00
Jaime Baez
79dea504b0 Add e2e testing setup (#163)
* Setup e2e testing

* Add user e2e tests

* Rename database host env variable to DB_HOST

* Force push (try to recover DB_HOST env)

* Rename db host env variable to `DB_HOSTNAME`

* Remove unnecessary `initDb` from test-utils

The current database.config is running the migrations:
`migrationsRun: true`
2022-05-19 18:30:47 -05:00
Migelo
4900fecd10 fix immich-server service name in README (#166) 2022-05-18 06:26:37 -04:00
Alex
adfaab7eb2 Update to flutter 3 (#162) 2022-05-14 09:25:19 -05:00
Alex
c5adbea6e1 Fixed incorrect microservices URLs after updating dockerfiles (#159) 2022-05-11 06:18:11 -05:00
Alex
bb89fa4aab Modify docker-compose to be compatible with k8s (#149)
* Modify docker-compose file using a hyphen for services instead of underscore

* Change URL in Nginx setting
2022-05-08 07:07:58 -05:00
Alex
43d639104d Bug/fixed permission not requested android 10 (#150)
* Added  android:requestLegacyExternalStorage=true to manifest

* Up pubspec version code for android build
2022-05-08 06:47:38 -05:00
Alex
a1792a7d94 Pump docker-compose container version 2022-05-06 07:47:19 -05:00
290 changed files with 12750 additions and 2094 deletions

View File

@@ -1,15 +1,26 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: "[BUG]" title: '[BUG] <title>'
labels: bug labels: bug, need triage
assignees: '' assignees: ''
--- ---
<!--
Note: Please search to see if an issue already exists for the bug you encountered.
-->
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**Task List**
- [ ] I have read thoroughly the README setup and installation instructions.
- [ ] If my setup is different, I have included my docker-compose file.
- [ ] I have included my redacted `.env` file.
- [ ] I have included information on my machine, and environment.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,32 @@
name: Feature Request
description: Request a feature that you would like for the app
title: "[Feature]: "
labels: ["feature", "need triage"]
assignees:
- ""
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
- type: textarea
id: feature-detail
attributes:
label: Feature detail
placeholder: Describe the feature you would like to see for the app
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: Choose the platform for the feature
options:
- Web
- Mobile App
- Server
validations:
required: true

View File

@@ -4,18 +4,18 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [main] branches: [main]
pull_request:
branches: [main]
jobs: jobs:
# This image include both the server and microservices - the two containers can be slitted into separated
build_and_push_server_latest: # service with its coressponding entry file.
build_and_push_server_monorepo_latest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: "main" # branch fetch-depth: 0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0 uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -26,39 +26,68 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich - name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.0.0 uses: docker/build-push-action@v3.0.0
with: with:
context: ./server context: ./server
file: ./server/Dockerfile file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }} push: true
tags: | tags: |
altran1502/immich-server:latest altran1502/immich-server:latest
build_and_push_microservice_latest: build_and_push_machine_learning_latest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: "main" # branch fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0 - name: Set up QEMU
- name: Set up Docker Buildx uses: docker/setup-qemu-action@v2.0.0
id: buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.0.0 id: buildx
- name: Login to Docker Hub uses: docker/setup-buildx-action@v2.0.0
uses: docker/login-action@v2 - name: Login to Docker Hub
with: uses: docker/login-action@v2
username: ${{ secrets.DOCKERHUB_USERNAME }} with:
password: ${{ secrets.DOCKERHUB_TOKEN }} username: ${{ secrets.DOCKERHUB_USERNAME }}
- name: Build and Push Microservices password: ${{ secrets.DOCKERHUB_TOKEN }}
uses: docker/build-push-action@v3.0.0 - name: Build and Push Machine Learning
with: uses: docker/build-push-action@v3.0.0
context: ./microservices with:
file: ./microservices/Dockerfile context: ./machine-learning
platforms: linux/arm/v7,linux/amd64 file: ./machine-learning/Dockerfile
push: ${{ github.event_name != 'pull_request' }} platforms: linux/arm/v7,linux/amd64
tags: | push: true
altran1502/immich-microservices:latest tags: |
altran1502/immich-machine-learning:latest
build_and_push_web_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web
uses: docker/build-push-action@v3.0.0
with:
context: ./web
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod
push: true
tags: |
altran1502/immich-web:latest

View File

@@ -0,0 +1,95 @@
name: Build and Push Docker Image - Staging
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# This image include both the server and microservices - the two containers can be slitted into separated
# service with its coressponding entry file.
build_and_push_server_monorepo_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.0.0
with:
context: ./server
file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' }}
tags: |
altran1502/immich-server:staging
build_and_push_machine_learning_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.0.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name == 'pull_request' }}
tags: |
altran1502/immich-machine-learning:staging
build_and_push_web_staging:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web
uses: docker/build-push-action@v3.0.0
with:
context: ./web
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod
push: ${{ github.event_name == 'pull_request' }}
tags: |
altran1502/immich-web:staging

View File

@@ -12,14 +12,14 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: "main" ref: "main"
fetch-depth: 0 fetch-depth: 0
- name: 'Get Previous tag' - name: "Get Previous tag"
id: previoustag id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1" uses: "WyriHaximus/github-action-get-previous-tag@v1"
with: with:
fallback: latest fallback: latest
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0 uses: docker/setup-qemu-action@v2.0.0
@@ -50,14 +50,14 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: "main" ref: "main"
fetch-depth: 0 fetch-depth: 0
- name: 'Get Previous tag' - name: "Get Previous tag"
id: previoustag id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1" uses: "WyriHaximus/github-action-get-previous-tag@v1"
with: with:
fallback: latest fallback: latest
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0 uses: docker/setup-qemu-action@v2.0.0
@@ -80,4 +80,43 @@ jobs:
platforms: linux/arm/v7,linux/amd64 platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: | tags: |
altran1502/immich-microservices:${{ steps.previoustag.outputs.tag }} altran1502/immich-microservices:${{ steps.previoustag.outputs.tag }}
build_and_push_web_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main"
fetch-depth: 0
- name: "Get Previous tag"
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-web release
uses: docker/build-push-action@v3.0.0
with:
context: ./web
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
target: prod
tags: |
altran1502/immich-web:${{ steps.previoustag.outputs.tag }}

View File

@@ -5,7 +5,13 @@ dev-update:
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
dev-scale: dev-scale:
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans
stage:
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
test-e2e:
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich_server_test --remove-orphans
prod: prod:
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans

9
NOTES.md Normal file
View File

@@ -0,0 +1,9 @@
# TODO
Server scenario with web
[ ] 1 user exist without admin right -> make admin on first check
[ ] 2 users exist without admin right -> ask user to choose which account will be the admin
[ X ] No users exist -> prompt signup form for Admin

View File

@@ -31,6 +31,7 @@ Loading ~4000 images/videos
## Screenshots ## Screenshots
### Mobile client
<p align="left"> <p align="left">
<img src="design/login-screen.png" width="150" title="Login With Custom URL"> <img src="design/login-screen.png" width="150" title="Login With Custom URL">
<img src="design/backup-screen.png" width="150" title="Backup Setting Info"> <img src="design/backup-screen.png" width="150" title="Backup Setting Info">
@@ -39,14 +40,18 @@ Loading ~4000 images/videos
<img src="design/search-screen.jpeg" width="150" title="Curated Search Info"> <img src="design/search-screen.jpeg" width="150" title="Curated Search Info">
<img src="design/shared-albums.png" width="150" title="Shared Albums"> <img src="design/shared-albums.png" width="150" title="Shared Albums">
<img src="design/nsc6.png" width="150" title="EXIF Info"> <img src="design/nsc6.png" width="150" title="EXIF Info">
</p>
### Web client
<p align="center">
<img src="design/dashboard_photos.jpeg" width="100%" title="Home Dashboard">
</p> </p>
# Note # Note
**!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!** **!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
This project is under heavy development, there will be continous functions, features and api changes. This project is under heavy development, there will be continuous functions, features and api changes.
# Features # Features
@@ -67,29 +72,30 @@ This project is under heavy development, there will be continous functions, feat
- Show curated objects on the search page - Show curated objects on the search page
- Shared album with users on the same server - Shared album with users on the same server
- Selective backup - albums can be included and excluded during the backup process. - Selective backup - albums can be included and excluded during the backup process.
- Web interface is available for administrative tasks (creating new users) and viewing assets on the server - additional features are coming.
# System Requirement # System Requirement
**OS**: Preferred Linux-based operating system (Ubuntu, Debian, MacOS...etc). **OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
I haven't tested with `Docker for Windows` as well as `WSL` on Windows I haven't tested with `Docker for Windows` as well as `WSL` on Windows
*Raspberry Pi can be used but `microservices` container has to be comment out in `docker-compose` since TensorFlow has not been supported in Dockec image on arm64v7 yet.* *Raspberry Pi can be used but `microservices` container has to be comment out in `docker-compose` since TensorFlow has not been supported in Docker image on arm64v7 yet.*
**RAM**: At least 2GB, preffered 4GB. **RAM**: At least 2GB, preffered 4GB.
**Core**: At least 2 cores, preffered 4 cores. **Core**: At least 2 cores, preffered 4 cores.
# Development and Testing out the application # Getting Started
You can use docker compose for development and testing out the application, there are several services that compose Immich: You can use docker compose for development and testing out the application, there are several services that compose Immich:
1. **NestJs** - Backend of the application 1. **NestJs** - Backend of the application
2. **PostgreSQL** - Main database of the application 2. **SvelteKit** - Web frontend of the application
3. **Redis** - For sharing websocket instance between docker instances and background tasks message queue. 3. **PostgreSQL** - Main database of the application
4. **Nginx** - Load balancing and optimized file uploading. 4. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
5. **TensorFlow** - Object Detection and Image Classification. 5. **Nginx** - Load balancing and optimized file uploading.
6. **TensorFlow** - Object Detection and Image Classification.
## Step 1: Populate .env file ## Step 1: Populate .env file
@@ -108,52 +114,75 @@ Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is own
**Example** **Example**
```bash ```bash
###################################################################################
# Database # Database
###################################################################################
DB_USERNAME=postgres DB_USERNAME=postgres
DB_PASSWORD=postgres DB_PASSWORD=postgres
DB_DATABASE_NAME=immich DB_DATABASE_NAME=immich
###################################################################################
# Upload File Config # Upload File Config
###################################################################################
UPLOAD_LOCATION=<put-the-path-of-the-upload-folder-here> UPLOAD_LOCATION=<put-the-path-of-the-upload-folder-here>
###################################################################################
# JWT SECRET # JWT SECRET
###################################################################################
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
###################################################################################
# MAPBOX # MAPBOX
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY ####################################################################################
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
ENABLE_MAPBOX=false ENABLE_MAPBOX=false
MAPBOX_KEY= MAPBOX_KEY=
###################################################################################
# WEB
###################################################################################
# This is the URL of your vm/server where you host Immich, so that the web frontend
# know where can it make the request to.
# For example: If your server IP address is 10.1.11.50, the environment variable will
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283
``` ```
## Step 2: Start the server ## Step 2: Start the server
To start, run To **start**, run
```bash ```bash
docker-compose -f ./docker/docker-compose.yml up docker-compose -f ./docker/docker-compose.yml up
``` ```
If you have a few thousand photos/videos, I suggest running docker-compose with scaling option for the `immich_server` container to handle high I/O load when using fast scrolling. If you have a few thousand photos/videos, I suggest running docker-compose with *scaling* option for the `immich_server` container to handle high I/O load when using fast scrolling.
```bash ```bash
docker-compose -f ./docker/docker-compose.yml up --scale immich_server=5 docker-compose -f ./docker/docker-compose.yml up --scale immich-server=5
``` ```
To *update* docker-compose with newest image (if you have started the docker-compose previously)
```bash
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
```
The server will be running at `http://your-ip:2283` through `Nginx` The server will be running at `http://your-ip:2283` through `Nginx`
## Step 3: Register User ## Step 3: Register User
Use the command below on your terminal to create user as we don't have user interface for this function yet. Access the web interface at `http://your-ip:2285` to register an admin account.
```bash <p align="left">
curl --location --request POST 'http://your-server-ip:2283/auth/signUp' \ <img src="design/admin-registration-form.png" width="300" title="Admin Registration">
--header 'Content-Type: application/json' \ <p/>
--data-raw '{
"email": "testuser@email.com", Additional accounts on the server can be created by the admin account.
"password": "password"
}' <p align="left">
``` <img src="design/admin-interface.png" width="500" title="Admin User Management">
<p/>
## Step 4: Run mobile app ## Step 4: Run mobile app
@@ -188,15 +217,26 @@ You can get the app on F-droid by clicking the image below.
<img src="design/ios-qr-code.png" width="200" title="Apple App Store"> <img src="design/ios-qr-code.png" width="200" title="Apple App Store">
<p/> <p/>
# Development
The development environment can be started from the root of the project after populating the `.env` file with the command:
```bash
make dev # required Makefile installed on the system.
```
All servers and web container are hot reload for quick feedback loop.
# Support # Support
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsore**](https://github.com/sponsors/alextran1502), or one time donation with Buy Me a coffee link below. If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below.
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/altran1502) [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/altran1502)
This is also a meaningful way to give me motivation and encounragment to continue working on the app. This is also a meaningful way to give me motivation and encouragement to continue working on the app.
Cheer! 🎉 Cheers! 🎉
# Known Issue # Known Issue
@@ -204,13 +244,13 @@ Cheer! 🎉
*This is a known issue on RaspberryPi 4 arm64-v7 and incorrect Promox setup* *This is a known issue on RaspberryPi 4 arm64-v7 and incorrect Promox setup*
TensorFlow doesn't run with older CPU architecture, it requires CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`: TensorFlow doesn't run with older CPU architecture, it requires a CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`:
```bash ```bash
more /proc/cpuinfo | grep flags more /proc/cpuinfo | grep flags
``` ```
If you are running virtualization in Promox, the VM doesn't have the flag enable. If you are running virtualization in Promox, the VM doesn't have the flag enabled.
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab. You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.

BIN
design/admin-interface.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@@ -1,15 +1,63 @@
###################################################################################
# Database # Database
###################################################################################
DB_HOSTNAME=immich_postgres
DB_USERNAME=postgres DB_USERNAME=postgres
DB_PASSWORD=postgres DB_PASSWORD=postgres
DB_DATABASE_NAME=immich DB_DATABASE_NAME=immich
###################################################################################
# Redis
###################################################################################
REDIS_HOSTNAME=immich_redis
###################################################################################
# Upload File Config # Upload File Config
###################################################################################
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
###################################################################################
# JWT SECRET # JWT SECRET
###################################################################################
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
###################################################################################
# MAPBOX # MAPBOX
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY ####################################################################################
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
ENABLE_MAPBOX=false ENABLE_MAPBOX=false
MAPBOX_KEY= MAPBOX_KEY=
###################################################################################
# WEB
###################################################################################
# This is the URL of your vm/server where you host Immich, so that the web frontend
# know where can it make the request to.
# For example: If your server IP address is 10.1.11.50, the environment variable will
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
VITE_SERVER_ENDPOINT=

22
docker/.env.test Normal file
View File

@@ -0,0 +1,22 @@
# Database
DB_HOSTNAME=immich_postgres_test
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=e2e_test
# Redis
REDIS_HOSTNAME=immich_redis_test
# Upload File Config
UPLOAD_LOCATION=./upload
# JWT SECRET
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
# MAPBOX
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
ENABLE_MAPBOX=false
# WEB
MAPBOX_KEY=
VITE_SERVER_ENDPOINT=http://localhost:2283

View File

@@ -1,12 +1,12 @@
version: "3.8" version: "3.8"
services: services:
immich_server: immich-server:
image: immich-server-dev:1.9.0 image: immich-server-dev:1.9.0
build: build:
context: ../server context: ../server
dockerfile: Dockerfile dockerfile: Dockerfile
command: npm run start:dev command: npm run start:dev immich
expose: expose:
- "3000" - "3000"
volumes: volumes:
@@ -21,18 +21,18 @@ services:
- redis - redis
- database - database
networks: networks:
- immich_network - immich-network
immich_microservices: immich-machine-learning:
image: immich-microservices-dev:1.9.0 image: immich-machine-learning-dev:1.9.0
build: build:
context: ../microservices context: ../machine-learning
dockerfile: Dockerfile dockerfile: Dockerfile
command: npm run start:dev command: npm run start:dev
expose: expose:
- "3001" - "3001"
volumes: volumes:
- ../microservices:/usr/src/app - ../machine-learning:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules - /usr/src/app/node_modules
env_file: env_file:
@@ -42,14 +42,51 @@ services:
depends_on: depends_on:
- database - database
networks: networks:
- immich_network - immich-network
immich-microservices:
image: immich-microservices:1.9.0
build:
context: ../server
dockerfile: Dockerfile
command: npm run start:dev microservices
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
networks:
- immich-network
immich-web:
image: immich-web-dev:1.9.0
build:
context: ../web
dockerfile: Dockerfile
target: dev
command: npm run dev --host
env_file:
- .env
ports:
- 3002:3002
- 24678:24678
volumes:
- ../web:/usr/src/app
- /usr/src/app/node_modules
networks:
- immich-network
restart: always
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2 image: redis:6.2
networks: networks:
- immich_network - immich-network
database: database:
container_name: immich_postgres container_name: immich_postgres
@@ -66,7 +103,7 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
networks: networks:
- immich_network - immich-network
nginx: nginx:
container_name: proxy_nginx container_name: proxy_nginx
@@ -79,11 +116,11 @@ services:
logging: logging:
driver: none driver: none
networks: networks:
- immich_network - immich-network
depends_on: depends_on:
- immich_server - immich-server
networks: networks:
immich_network: immich-network:
volumes: volumes:
pgdata: pgdata:

View File

@@ -1,7 +1,7 @@
version: "3.8" version: "3.8"
services: services:
immich_server: immich-server:
image: immich-server-dev:1.9.0 image: immich-server-dev:1.9.0
build: build:
context: ../server context: ../server
@@ -19,9 +19,9 @@ services:
- redis - redis
- database - database
networks: networks:
- immich_network - immich-network
immich_microservices: immich-microservices:
image: immich-microservices-dev:1.9.0 image: immich-microservices-dev:1.9.0
build: build:
context: ../microservices context: ../microservices
@@ -46,13 +46,13 @@ services:
- database - database
- immich_server - immich_server
networks: networks:
- immich_network - immich-network
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2 image: redis:6.2
networks: networks:
- immich_network - immich-network
database: database:
container_name: immich_postgres container_name: immich_postgres
@@ -69,7 +69,7 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
networks: networks:
- immich_network - immich-network
nginx: nginx:
container_name: proxy_nginx container_name: proxy_nginx
@@ -82,11 +82,11 @@ services:
logging: logging:
driver: none driver: none
networks: networks:
- immich_network - immich-network
depends_on: depends_on:
- immich_server - immich-server
networks: networks:
immich_network: immich-network:
volumes: volumes:
pgdata: pgdata:

View File

@@ -0,0 +1,110 @@
version: "3.8"
services:
immich-server:
image: altran1502/immich-server:staging
entrypoint: ["/bin/sh", "./start-server.sh"]
expose:
- "3000"
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- redis
- database
networks:
- immich-network
restart: always
immich-microservices:
image: altran1502/immich-server:staging
entrypoint: ["/bin/sh", "./start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- redis
- database
networks:
- immich-network
restart: always
immich-machine-learning:
image: altran1502/immich-machine-learning:staging
entrypoint: ["/bin/sh", "./entrypoint.sh"]
expose:
- "3001"
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- database
networks:
- immich-network
restart: always
immich-web:
image: altran1502/immich-web:staging
entrypoint: ["/bin/sh", "./entrypoint.sh"]
env_file:
- .env
ports:
- 2285:3000
networks:
- immich-network
restart: always
redis:
container_name: immich_redis
image: redis:6.2
networks:
- immich-network
restart: always
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
restart: always
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
restart: always
networks:
immich-network:
volumes:
pgdata:

View File

@@ -0,0 +1,50 @@
version: "3.8"
services:
immich_server_test:
image: immich-server-dev:1.9.0
build:
context: ../server
dockerfile: Dockerfile
command: npm run test:e2e
expose:
- "3000"
volumes:
- ../server:/usr/src/app
- /usr/src/app/node_modules
env_file:
- .env.test
environment:
- NODE_ENV=development
depends_on:
- redis
- database
networks:
- immich_network_test
redis:
container_name: immich_redis_test
image: redis:6.2
networks:
- immich_network_test
database:
container_name: immich_postgres_test
image: postgres:14
env_file:
- .env.test
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
PG_DATA: /var/lib/postgresql/data
volumes:
- /var/lib/postgresql/data
ports:
- 5432:5432
networks:
- immich_network_test
networks:
immich_network_test:

View File

@@ -1,9 +1,9 @@
version: "3.8" version: "3.8"
services: services:
immich_server: immich-server:
image: altran1502/immich-server:v1.8.0_12-dev image: altran1502/immich-server:latest
entrypoint: ["/bin/sh", "./entrypoint.sh"] entrypoint: ["/bin/sh", "./start-server.sh"]
expose: expose:
- "3000" - "3000"
volumes: volumes:
@@ -16,11 +16,27 @@ services:
- redis - redis
- database - database
networks: networks:
- immich_network - immich-network
restart: unless-stopped restart: always
immich_microservices: immich-microservices:
image: altran1502/immich-microservices:v1.8.0_12-dev image: altran1502/immich-server:latest
entrypoint: ["/bin/sh", "./start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- redis
- database
networks:
- immich-network
restart: always
immich-machine-learning:
image: altran1502/immich-machine-learning:latest
entrypoint: ["/bin/sh", "./entrypoint.sh"] entrypoint: ["/bin/sh", "./entrypoint.sh"]
expose: expose:
- "3001" - "3001"
@@ -33,14 +49,26 @@ services:
depends_on: depends_on:
- database - database
networks: networks:
- immich_network - immich-network
restart: unless-stopped restart: always
immich-web:
image: altran1502/immich-web:latest
entrypoint: ["/bin/sh", "./entrypoint.sh"]
env_file:
- .env
ports:
- 2285:3000
networks:
- immich-network
restart: always
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2 image: redis:6.2
networks: networks:
- immich_network - immich-network
restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
@@ -57,7 +85,8 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
networks: networks:
- immich_network - immich-network
restart: always
nginx: nginx:
container_name: proxy_nginx container_name: proxy_nginx
@@ -70,11 +99,12 @@ services:
logging: logging:
driver: none driver: none
networks: networks:
- immich_network - immich-network
depends_on: depends_on:
- immich_server - immich-server
restart: always
networks: networks:
immich_network: immich-network:
volumes: volumes:
pgdata: pgdata:

View File

@@ -41,6 +41,6 @@ server {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_pass http://immich_server:3000; proxy_pass http://immich-server:3000;
} }
} }

View File

@@ -32,4 +32,6 @@ lerna-debug.log*
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
upload/

View File

@@ -2,7 +2,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export const databaseConfig: TypeOrmModuleOptions = { export const databaseConfig: TypeOrmModuleOptions = {
type: 'postgres', type: 'postgres',
host: 'immich_postgres', host: process.env.DB_HOSTNAME || 'immich_postgres',
port: 5432, port: 5432,
username: process.env.DB_USERNAME, username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,

View File

@@ -5,9 +5,9 @@ import { ImageClassifierService } from './image-classifier.service';
export class ImageClassifierController { export class ImageClassifierController {
constructor( constructor(
private readonly imageClassifierService: ImageClassifierService, private readonly imageClassifierService: ImageClassifierService,
) {} ) { }
@Post('/tagImage') @Post('/tag-image')
async tagImage(@Body('thumbnailPath') thumbnailPath: string) { async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
return await this.imageClassifierService.tagImage(thumbnailPath); return await this.imageClassifierService.tagImage(thumbnailPath);
} }

View File

@@ -8,14 +8,14 @@ async function bootstrap() {
await app.listen(3001, () => { await app.listen(3001, () => {
if (process.env.NODE_ENV == 'development') { if (process.env.NODE_ENV == 'development') {
Logger.log( Logger.log(
'Running Immich Microservices in DEVELOPMENT environment', 'Running Immich Machine Learning in DEVELOPMENT environment',
'IMMICH MICROSERVICES', 'IMMICH MICROSERVICES',
); );
} }
if (process.env.NODE_ENV == 'production') { if (process.env.NODE_ENV == 'production') {
Logger.log( Logger.log(
'Running Immich Microservices in PRODUCTION environment', 'Running Immich Machine Learning in PRODUCTION environment',
'IMMICH MICROSERVICES', 'IMMICH MICROSERVICES',
); );
} }

View File

@@ -1,13 +1,14 @@
import { Body, Controller, Post } from '@nestjs/common'; import { Body, Controller, Post } from '@nestjs/common';
import { ObjectDetectionService } from './object-detection.service'; import { ObjectDetectionService } from './object-detection.service';
import { Logger } from '@nestjs/common';
@Controller('object-detection') @Controller('object-detection')
export class ObjectDetectionController { export class ObjectDetectionController {
constructor( constructor(
private readonly objectDetectionService: ObjectDetectionService, private readonly objectDetectionService: ObjectDetectionService,
) {} ) { }
@Post('/detectObject') @Post('/detect-object')
async detectObject(@Body('thumbnailPath') thumbnailPath: string) { async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
return await this.objectDetectionService.detectObject(thumbnailPath); return await this.objectDetectionService.detectObject(thumbnailPath);
} }

View File

@@ -1 +0,0 @@
devenv/

View File

@@ -1,3 +0,0 @@
__pycache__/
devenv/
app/upload

View File

@@ -1,25 +0,0 @@
## GPU Build
# FROM tensorflow/tensorflow:latest-gpu as gpu
# WORKDIR /code
# COPY ./requirements.txt /code/requirements.txt
# RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# COPY ./app /code/app
## CPU BUILD
FROM python:3.8 as cpu
RUN apt-get update
RUN apt-get install ffmpeg libsm6 libxext6 -y
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -1,37 +0,0 @@
from tensorflow.keras.applications import InceptionV3
from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image
import numpy as np
from PIL import Image
import cv2
IMG_SIZE = 299
PREDICTION_MODEL = InceptionV3(weights='imagenet')
def classify_image(image_path: str):
img_path = f'./app/{image_path}'
# img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE))
target_image = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)
resized_target_image = cv2.resize(target_image, (IMG_SIZE, IMG_SIZE))
x = image.img_to_array(resized_target_image)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
preds = PREDICTION_MODEL.predict(x)
result = decode_predictions(preds, top=3)[0]
payload = []
for _, value, _ in result:
payload.append(value)
return payload
def warm_up():
img_path = f'./app/test.png'
img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
PREDICTION_MODEL.predict(x)

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
from pydantic import BaseModel
from fastapi import FastAPI
from .object_detection import object_detection
from .image_classifier import image_classifier
from tf2_yolov4.anchors import YOLOV4_ANCHORS
from tf2_yolov4.model import YOLOv4
HEIGHT, WIDTH = (640, 960)
# Warm up model
image_classifier.warm_up()
app = FastAPI()
class TagImagePayload(BaseModel):
thumbnail_path: str
@app.post("/tagImage")
async def post_root(payload: TagImagePayload):
image_path = payload.thumbnail_path
if image_path[0] == '.':
image_path = image_path[2:]
return image_classifier.classify_image(image_path=image_path)
@app.get("/")
async def test():
object_detection.run_detection()
# image = tf.io.read_file("./app/cars.jpg")
# image = tf.image.decode_image(image)
# image = tf.image.resize(image, (HEIGHT, WIDTH))
# images = tf.expand_dims(image, axis=0) / 255.0
# model = YOLOv4(
# (HEIGHT, WIDTH, 3),
# 80,
# YOLOV4_ANCHORS,
# "darknet",
# )

View File

@@ -1,4 +0,0 @@
def run_detection():
print("run detection")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

View File

@@ -1,8 +0,0 @@
opencv-python==4.5.5.64
fastapi>=0.68.0,<0.69.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0
tensorflow==2.8.0
numpy==1.22.2
pillow==9.0.1
tf2_yolov4==0.1.0

View File

@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
<application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher"> <application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
@@ -23,4 +23,11 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
</manifest> </manifest>

View File

@@ -1,16 +1,21 @@
// Generated file. // Generated file.
//
// If you wish to remove Flutter's multidex support, delete this entire file. // If you wish to remove Flutter's multidex support, delete this entire file.
//
// Modifications to this file should be done in a copy under a different name
// as this file may be regenerated.
package io.flutter.app; package io.flutter.app;
import android.app.Application;
import android.content.Context; import android.content.Context;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex; import androidx.multidex.MultiDex;
/** /**
* Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support. * Extension of {@link android.app.Application}, adding multidex support.
*/ */
public class FlutterMultiDexApplication extends FlutterApplication { public class FlutterMultiDexApplication extends Application {
@Override @Override
@CallSuper @CallSuper
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {

View File

@@ -0,0 +1 @@
* Hotfix: Permission is being requested now when open backup screen on Android10

View File

@@ -0,0 +1 @@
* User can now upload profile picture from the home page control drawer.

View File

@@ -0,0 +1,2 @@
* Update to Material Design 3
* Fixed back button navigation - no longer return back to the home page

View File

@@ -0,0 +1 @@
* Added announcement pop-up when a new released is pushed out in Github.

View File

@@ -9,6 +9,8 @@ PODS:
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
- image_picker_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_ios (0.0.1): - path_provider_ios (0.0.1):
@@ -30,6 +32,7 @@ DEPENDENCIES:
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
@@ -50,6 +53,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_udid/ios" :path: ".symlinks/plugins/flutter_udid/ios"
fluttertoast: fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios" :path: ".symlinks/plugins/fluttertoast/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios: path_provider_ios:
@@ -66,10 +71,11 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58 fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904

View File

@@ -360,7 +360,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -495,7 +495,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -522,7 +522,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>1.10.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>2</string> <string>14</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true /> <true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key> <key>MGLMapboxMetricsEnabledSettingShownInApp</key>
@@ -43,6 +43,12 @@
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string> <string>We need to manage backup your photos album</string>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access the microphone to let you take beautiful video using this app</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@@ -52,7 +58,7 @@
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
@@ -68,5 +74,13 @@
<true /> <true />
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false /> <false />
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.9.0" version_number: "1.11.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,

View File

@@ -21,7 +21,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
[bundle exec] fastlane ios beta [bundle exec] fastlane ios beta
``` ```
iOS deployment iOS Beta
---- ----

View File

@@ -5,27 +5,12 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000332"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000946">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: latest_testflight_build_number" time="4.608292"> <testcase classname="fastlane.lanes" name="1: increment_version_number" time="16.3225">
</testcase>
<testcase classname="fastlane.lanes" name="2: increment_build_number" time="0.747162">
</testcase>
<testcase classname="fastlane.lanes" name="3: build_app" time="88.727281">
</testcase>
<testcase classname="fastlane.lanes" name="4: upload_to_testflight" time="7.79397">
</testcase> </testcase>

View File

@@ -13,3 +13,7 @@ const String savedLoginInfoKey = "immichSavedLoginInfoKey";
// Backup Info // Backup Info
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox"; const String hiveBackupInfoBox = "immichBackupAlbumInfoBox";
const String backupInfoKey = "immichBackupAlbumInfoKey"; const String backupInfoKey = "immichBackupAlbumInfoKey";
// Github Release Info
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox";
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey";

View File

@@ -5,14 +5,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart';
import 'package:immich_mobile/shared/providers/app_state.provider.dart'; import 'package:immich_mobile/shared/providers/app_state.provider.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'constants/hive_box.dart'; import 'constants/hive_box.dart';
void main() async { void main() async {
@@ -24,6 +27,7 @@ void main() async {
await Hive.openBox(userInfoBox); await Hive.openBox(userInfoBox);
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox); await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox); await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
await Hive.openBox(hiveGithubReleaseInfoBox);
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle( const SystemUiOverlayStyle(
@@ -48,10 +52,18 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
debugPrint("[APP STATE] resumed"); debugPrint("[APP STATE] resumed");
ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed; ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed;
ref.watch(backupProvider.notifier).resumeBackup();
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
if (isAuthenticated) {
ref.watch(backupProvider.notifier).resumeBackup();
ref.watch(assetProvider.notifier).getAllAsset();
ref.watch(serverInfoProvider.notifier).getServerVersion();
}
ref.watch(websocketProvider.notifier).connect(); ref.watch(websocketProvider.notifier).connect();
ref.watch(assetProvider.notifier).getAllAsset();
ref.watch(serverInfoProvider.notifier).getServerVersion(); ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
break; break;
@@ -76,7 +88,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
} }
Future<void> initApp() async { Future<void> initApp() async {
WidgetsBinding.instance?.addObserver(this); WidgetsBinding.instance.addObserver(this);
} }
@override @override
@@ -87,7 +99,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance?.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@@ -95,6 +107,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: Stack( home: Stack(
@@ -103,6 +117,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
title: 'Immich', title: 'Immich',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
useMaterial3: true,
brightness: Brightness.light, brightness: Brightness.light,
primarySwatch: Colors.indigo, primarySwatch: Colors.indigo,
fontFamily: 'WorkSans', fontFamily: 'WorkSans',
@@ -120,6 +135,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]), routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
), ),
const ImmichLoadingOverlay(), const ImmichLoadingOverlay(),
const VersionAnnouncementOverlay(),
], ],
), ),
); );

View File

@@ -38,5 +38,7 @@ class HiveBackupAlbumsAdapter extends TypeAdapter<HiveBackupAlbums> {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is HiveBackupAlbumsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; other is HiveBackupAlbumsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
} }

View File

@@ -73,7 +73,7 @@ class BackupService {
}); });
// Build thumbnail multipart data // Build thumbnail multipart data
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280)); var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
if (thumbnailData != null) { if (thumbnailData != null) {
thumbnailUploadData = MultipartFile.fromBytes( thumbnailUploadData = MultipartFile.fromBytes(
List.from(thumbnailData), List.from(thumbnailData),

View File

@@ -45,12 +45,16 @@ class BackupControllerPage extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LinearPercentIndicator( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
lineHeight: 5.0, child: LinearPercentIndicator(
percent: backupState.serverInfo.diskUsagePercentage / 100.0, padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
backgroundColor: Colors.grey, barRadius: const Radius.circular(2),
progressColor: Theme.of(context).primaryColor, lineHeight: 6.0,
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
backgroundColor: Colors.grey,
progressColor: Theme.of(context).primaryColor,
),
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 12.0), padding: const EdgeInsets.only(top: 12.0),
@@ -92,6 +96,12 @@ class BackupControllerPage extends HookConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: OutlinedButton( child: OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(
width: 1,
color: Color.fromARGB(255, 220, 220, 220),
),
),
onPressed: () { onPressed: () {
isAutoBackup isAutoBackup
? ref.watch(authenticationProvider.notifier).setAutoBackup(false) ? ref.watch(authenticationProvider.notifier).setAutoBackup(false)
@@ -187,6 +197,13 @@ class BackupControllerPage extends HookConsumerWidget {
), ),
), ),
trailing: OutlinedButton( trailing: OutlinedButton(
style: OutlinedButton.styleFrom(
enableFeedback: true,
side: const BorderSide(
width: 1,
color: Color.fromARGB(255, 220, 220, 220),
),
),
onPressed: () { onPressed: () {
AutoRouter.of(context).push(const BackupAlbumSelectionRoute()); AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
}, },
@@ -274,13 +291,20 @@ class BackupControllerPage extends HookConsumerWidget {
child: Container( child: Container(
child: backupState.backupProgress == BackUpProgressEnum.inProgress child: backupState.backupProgress == BackUpProgressEnum.inProgress
? ElevatedButton( ? ElevatedButton(
style: ElevatedButton.styleFrom(primary: Colors.red[300]), style: ElevatedButton.styleFrom(
primary: Colors.red[300],
onPrimary: Colors.grey[50],
),
onPressed: () { onPressed: () {
ref.read(backupProvider.notifier).cancelBackup(); ref.read(backupProvider.notifier).cancelBackup();
}, },
child: const Text("Cancel"), child: const Text("Cancel"),
) )
: ElevatedButton( : ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).primaryColor,
onPrimary: Colors.grey[50],
),
onPressed: shouldBackup onPressed: shouldBackup
? () { ? () {
ref.read(backupProvider.notifier).startBackupProcess(); ref.read(backupProvider.notifier).startBackupProcess();

View File

@@ -0,0 +1,93 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/shared/services/user.service.dart';
enum UploadProfileStatus {
idle,
loading,
success,
failure,
}
class UploadProfileImageState {
// enum
final UploadProfileStatus status;
final String profileImagePath;
UploadProfileImageState({
required this.status,
required this.profileImagePath,
});
UploadProfileImageState copyWith({
UploadProfileStatus? status,
String? profileImagePath,
}) {
return UploadProfileImageState(
status: status ?? this.status,
profileImagePath: profileImagePath ?? this.profileImagePath,
);
}
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
result.addAll({'status': status.index});
result.addAll({'profileImagePath': profileImagePath});
return result;
}
factory UploadProfileImageState.fromMap(Map<String, dynamic> map) {
return UploadProfileImageState(
status: UploadProfileStatus.values[map['status'] ?? 0],
profileImagePath: map['profileImagePath'] ?? '',
);
}
String toJson() => json.encode(toMap());
factory UploadProfileImageState.fromJson(String source) => UploadProfileImageState.fromMap(json.decode(source));
@override
String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UploadProfileImageState && other.status == status && other.profileImagePath == profileImagePath;
}
@override
int get hashCode => status.hashCode ^ profileImagePath.hashCode;
}
class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState> {
UploadProfileImageNotifier()
: super(UploadProfileImageState(
profileImagePath: '',
status: UploadProfileStatus.idle,
));
Future<bool> upload(XFile file) async {
state = state.copyWith(status: UploadProfileStatus.loading);
var res = await UserService().uploadProfileImage(file);
if (res != null) {
debugPrint("Succesfully upload profile image");
state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: res.profileImagePath);
return true;
}
state = state.copyWith(status: UploadProfileStatus.failure);
return false;
}
}
final uploadProfileImageProvider =
StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(((ref) => UploadProfileImageNotifier()));

View File

@@ -109,7 +109,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
? const Icon(Icons.backup_rounded) ? const Icon(Icons.backup_rounded)
: Badge( : Badge(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
elevation: 1, elevation: 2,
position: BadgePosition.bottomEnd(bottom: -4, end: -4), position: BadgePosition.bottomEnd(bottom: -4, end: -4),
badgeColor: Colors.white, badgeColor: Colors.white,
badgeContent: const Icon( badgeContent: const Icon(
@@ -117,7 +117,6 @@ class ImmichSliverAppBar extends ConsumerWidget {
size: 8, size: 8,
), ),
child: const Icon(Icons.backup_rounded)), child: const Icon(Icons.backup_rounded)),
tooltip: 'Backup Controller',
onPressed: () async { onPressed: () async {
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute()); var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());

View File

@@ -1,7 +1,11 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -9,17 +13,21 @@ import 'package:immich_mobile/shared/models/server_info_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'dart:math';
class ProfileDrawer extends HookConsumerWidget { class ProfileDrawer extends HookConsumerWidget {
const ProfileDrawer({Key? key}) : super(key: key); const ProfileDrawer({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
AuthenticationState _authState = ref.watch(authenticationProvider); AuthenticationState _authState = ref.watch(authenticationProvider);
ServerInfoState _serverInfoState = ref.watch(serverInfoProvider); ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status;
final appInfo = useState({}); final appInfo = useState({});
var dummmy = Random().nextInt(1024);
_getPackageInfo() async { _getPackageInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
@@ -30,19 +38,74 @@ class ProfileDrawer extends HookConsumerWidget {
}; };
} }
_buildUserProfileImage() {
if (_authState.profileImagePath.isEmpty) {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
if (_authState.profileImagePath.isNotEmpty) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
backgroundColor: Colors.transparent,
);
} else {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
}
if (uploadProfileImageStatus == UploadProfileStatus.success) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
return const ImmichLoadingIndicator();
}
return Container();
}
_pickUserProfileImage() async {
final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
if (image != null) {
var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);
if (success) {
ref
.watch(authenticationProvider.notifier)
.updateUserProfileImagePath(ref.read(uploadProfileImageProvider).profileImagePath);
}
}
}
useEffect(() { useEffect(() {
_getPackageInfo(); _getPackageInfo();
_buildUserProfileImage();
return null; return null;
}, []); }, []);
return Drawer( return Drawer(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(5),
bottomRight: Radius.circular(5),
),
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -51,22 +114,56 @@ class ProfileDrawer extends HookConsumerWidget {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
children: [ children: [
DrawerHeader( DrawerHeader(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.grey[200], gradient: LinearGradient(
colors: [Color.fromARGB(255, 216, 219, 238), Color.fromARGB(255, 226, 230, 231)],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Image( Stack(
image: AssetImage('assets/immich-logo-no-outline.png'), clipBehavior: Clip.none,
width: 50, children: [
filterQuality: FilterQuality.high, _buildUserProfileImage(),
Positioned(
bottom: 0,
right: -5,
child: GestureDetector(
onTap: _pickUserProfileImage,
child: Material(
color: Colors.grey[50],
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0),
),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Icon(
Icons.edit,
color: Theme.of(context).primaryColor,
size: 14,
),
),
),
),
),
],
),
Text(
"${_authState.firstName} ${_authState.lastName}",
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 24,
),
), ),
const Padding(padding: EdgeInsets.all(8)),
Text( Text(
_authState.userEmail, _authState.userEmail,
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold), style: TextStyle(color: Colors.grey[800], fontSize: 12),
) )
], ],
), ),
@@ -97,7 +194,15 @@ class ProfileDrawer extends HookConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Card( child: Card(
elevation: 0,
color: Colors.grey[100], color: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), // if you need this
side: const BorderSide(
color: Color.fromARGB(101, 201, 201, 201),
width: 1,
),
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
child: Column( child: Column(

View File

@@ -8,6 +8,11 @@ class AuthenticationState {
final String userId; final String userId;
final String userEmail; final String userEmail;
final bool isAuthenticated; final bool isAuthenticated;
final String firstName;
final String lastName;
final bool isAdmin;
final bool isFirstLogin;
final String profileImagePath;
final DeviceInfoRemote deviceInfo; final DeviceInfoRemote deviceInfo;
AuthenticationState({ AuthenticationState({
@@ -16,6 +21,11 @@ class AuthenticationState {
required this.userId, required this.userId,
required this.userEmail, required this.userEmail,
required this.isAuthenticated, required this.isAuthenticated,
required this.firstName,
required this.lastName,
required this.isAdmin,
required this.isFirstLogin,
required this.profileImagePath,
required this.deviceInfo, required this.deviceInfo,
}); });
@@ -25,6 +35,11 @@ class AuthenticationState {
String? userId, String? userId,
String? userEmail, String? userEmail,
bool? isAuthenticated, bool? isAuthenticated,
String? firstName,
String? lastName,
bool? isAdmin,
bool? isFirstLoggedIn,
String? profileImagePath,
DeviceInfoRemote? deviceInfo, DeviceInfoRemote? deviceInfo,
}) { }) {
return AuthenticationState( return AuthenticationState(
@@ -33,24 +48,36 @@ class AuthenticationState {
userId: userId ?? this.userId, userId: userId ?? this.userId,
userEmail: userEmail ?? this.userEmail, userEmail: userEmail ?? this.userEmail,
isAuthenticated: isAuthenticated ?? this.isAuthenticated, isAuthenticated: isAuthenticated ?? this.isAuthenticated,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
isAdmin: isAdmin ?? this.isAdmin,
isFirstLogin: isFirstLoggedIn ?? isFirstLogin,
profileImagePath: profileImagePath ?? this.profileImagePath,
deviceInfo: deviceInfo ?? this.deviceInfo, deviceInfo: deviceInfo ?? this.deviceInfo,
); );
} }
@override @override
String toString() { String toString() {
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, deviceInfo: $deviceInfo)'; return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, isFirstLoggedIn: $isFirstLogin, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)';
} }
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { final result = <String, dynamic>{};
'deviceId': deviceId,
'deviceType': deviceType, result.addAll({'deviceId': deviceId});
'userId': userId, result.addAll({'deviceType': deviceType});
'userEmail': userEmail, result.addAll({'userId': userId});
'isAuthenticated': isAuthenticated, result.addAll({'userEmail': userEmail});
'deviceInfo': deviceInfo.toMap(), result.addAll({'isAuthenticated': isAuthenticated});
}; result.addAll({'firstName': firstName});
result.addAll({'lastName': lastName});
result.addAll({'isAdmin': isAdmin});
result.addAll({'isFirstLogin': isFirstLogin});
result.addAll({'profileImagePath': profileImagePath});
result.addAll({'deviceInfo': deviceInfo.toMap()});
return result;
} }
factory AuthenticationState.fromMap(Map<String, dynamic> map) { factory AuthenticationState.fromMap(Map<String, dynamic> map) {
@@ -60,6 +87,11 @@ class AuthenticationState {
userId: map['userId'] ?? '', userId: map['userId'] ?? '',
userEmail: map['userEmail'] ?? '', userEmail: map['userEmail'] ?? '',
isAuthenticated: map['isAuthenticated'] ?? false, isAuthenticated: map['isAuthenticated'] ?? false,
firstName: map['firstName'] ?? '',
lastName: map['lastName'] ?? '',
isAdmin: map['isAdmin'] ?? false,
isFirstLogin: map['isFirstLogin'] ?? false,
profileImagePath: map['profileImagePath'] ?? '',
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']), deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
); );
} }
@@ -78,6 +110,11 @@ class AuthenticationState {
other.userId == userId && other.userId == userId &&
other.userEmail == userEmail && other.userEmail == userEmail &&
other.isAuthenticated == isAuthenticated && other.isAuthenticated == isAuthenticated &&
other.firstName == firstName &&
other.lastName == lastName &&
other.isAdmin == isAdmin &&
other.isFirstLogin == isFirstLogin &&
other.profileImagePath == profileImagePath &&
other.deviceInfo == deviceInfo; other.deviceInfo == deviceInfo;
} }
@@ -88,6 +125,11 @@ class AuthenticationState {
userId.hashCode ^ userId.hashCode ^
userEmail.hashCode ^ userEmail.hashCode ^
isAuthenticated.hashCode ^ isAuthenticated.hashCode ^
firstName.hashCode ^
lastName.hashCode ^
isAdmin.hashCode ^
isFirstLogin.hashCode ^
profileImagePath.hashCode ^
deviceInfo.hashCode; deviceInfo.hashCode;
} }
} }

View File

@@ -4,31 +4,58 @@ class LogInReponse {
final String accessToken; final String accessToken;
final String userId; final String userId;
final String userEmail; final String userEmail;
final String firstName;
final String lastName;
final String profileImagePath;
final bool isAdmin;
final bool isFirstLogin;
LogInReponse({ LogInReponse({
required this.accessToken, required this.accessToken,
required this.userId, required this.userId,
required this.userEmail, required this.userEmail,
required this.firstName,
required this.lastName,
required this.profileImagePath,
required this.isAdmin,
required this.isFirstLogin,
}); });
LogInReponse copyWith({ LogInReponse copyWith({
String? accessToken, String? accessToken,
String? userId, String? userId,
String? userEmail, String? userEmail,
String? firstName,
String? lastName,
String? profileImagePath,
bool? isAdmin,
bool? isFirstLogin,
}) { }) {
return LogInReponse( return LogInReponse(
accessToken: accessToken ?? this.accessToken, accessToken: accessToken ?? this.accessToken,
userId: userId ?? this.userId, userId: userId ?? this.userId,
userEmail: userEmail ?? this.userEmail, userEmail: userEmail ?? this.userEmail,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
profileImagePath: profileImagePath ?? this.profileImagePath,
isAdmin: isAdmin ?? this.isAdmin,
isFirstLogin: isFirstLogin ?? this.isFirstLogin,
); );
} }
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { final result = <String, dynamic>{};
'accessToken': accessToken,
'userId': userId, result.addAll({'accessToken': accessToken});
'userEmail': userEmail, result.addAll({'userId': userId});
}; result.addAll({'userEmail': userEmail});
result.addAll({'firstName': firstName});
result.addAll({'lastName': lastName});
result.addAll({'profileImagePath': profileImagePath});
result.addAll({'isAdmin': isAdmin});
result.addAll({'isFirstLogin': isFirstLogin});
return result;
} }
factory LogInReponse.fromMap(Map<String, dynamic> map) { factory LogInReponse.fromMap(Map<String, dynamic> map) {
@@ -36,6 +63,11 @@ class LogInReponse {
accessToken: map['accessToken'] ?? '', accessToken: map['accessToken'] ?? '',
userId: map['userId'] ?? '', userId: map['userId'] ?? '',
userEmail: map['userEmail'] ?? '', userEmail: map['userEmail'] ?? '',
firstName: map['firstName'] ?? '',
lastName: map['lastName'] ?? '',
profileImagePath: map['profileImagePath'] ?? '',
isAdmin: map['isAdmin'] ?? false,
isFirstLogin: map['isFirstLogin'] ?? false,
); );
} }
@@ -44,7 +76,9 @@ class LogInReponse {
factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source)); factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source));
@override @override
String toString() => 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail)'; String toString() {
return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, isFirstLogin: $isFirstLogin)';
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@@ -53,9 +87,23 @@ class LogInReponse {
return other is LogInReponse && return other is LogInReponse &&
other.accessToken == accessToken && other.accessToken == accessToken &&
other.userId == userId && other.userId == userId &&
other.userEmail == userEmail; other.userEmail == userEmail &&
other.firstName == firstName &&
other.lastName == lastName &&
other.profileImagePath == profileImagePath &&
other.isAdmin == isAdmin &&
other.isFirstLogin == isFirstLogin;
} }
@override @override
int get hashCode => accessToken.hashCode ^ userId.hashCode ^ userEmail.hashCode; int get hashCode {
return accessToken.hashCode ^
userId.hashCode ^
userEmail.hashCode ^
firstName.hashCode ^
lastName.hashCode ^
profileImagePath.hashCode ^
isAdmin.hashCode ^
isFirstLogin.hashCode;
}
} }

View File

@@ -17,9 +17,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
AuthenticationState( AuthenticationState(
deviceId: "", deviceId: "",
deviceType: "", deviceType: "",
isAuthenticated: false,
userId: "", userId: "",
userEmail: "", userEmail: "",
firstName: '',
lastName: '',
profileImagePath: '',
isAdmin: false,
isFirstLogin: false,
isAuthenticated: false,
deviceInfo: DeviceInfoRemote( deviceInfo: DeviceInfoRemote(
id: 0, id: 0,
userId: "", userId: "",
@@ -76,6 +81,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
isAuthenticated: true, isAuthenticated: true,
userId: payload.userId, userId: payload.userId,
userEmail: payload.userEmail, userEmail: payload.userEmail,
firstName: payload.firstName,
lastName: payload.lastName,
profileImagePath: payload.profileImagePath,
isAdmin: payload.isAdmin,
isFirstLoggedIn: payload.isFirstLogin,
); );
if (isSavedLoginInfo) { if (isSavedLoginInfo) {
@@ -114,9 +124,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
state = AuthenticationState( state = AuthenticationState(
deviceId: "", deviceId: "",
deviceType: "", deviceType: "",
isAuthenticated: false,
userId: "", userId: "",
userEmail: "", userEmail: "",
firstName: '',
lastName: '',
profileImagePath: '',
isFirstLogin: false,
isAuthenticated: false,
isAdmin: false,
deviceInfo: DeviceInfoRemote( deviceInfo: DeviceInfoRemote(
id: 0, id: 0,
userId: "", userId: "",
@@ -139,6 +154,10 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType); DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
state = state.copyWith(deviceInfo: deviceInfoRemote); state = state.copyWith(deviceInfo: deviceInfoRemote);
} }
updateUserProfileImagePath(String path) {
state = state.copyWith(profileImagePath: path);
}
} }
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) { final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {

View File

@@ -153,9 +153,12 @@ class LoginButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton( return ElevatedButton(
style: ButtonStyle( style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.standard, visualDensity: VisualDensity.standard,
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(vertical: 10, horizontal: 25)), primary: Theme.of(context).primaryColor,
onPrimary: Colors.grey[50],
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
), ),
onPressed: () async { onPressed: () async {
// This will remove current cache asset state of previous user login. // This will remove current cache asset state of previous user login.

View File

@@ -79,14 +79,14 @@ class SearchResultPage extends HookConsumerWidget {
return Chip( return Chip(
label: Wrap( label: Wrap(
spacing: 5, spacing: 5,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.center,
children: [ children: [
Padding( Text(
padding: const EdgeInsets.only(top: 2.0), currentSearchTerm.value,
child: Text( style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 13, fontWeight: FontWeight.bold),
currentSearchTerm.value, maxLines: 1,
style: TextStyle(color: Theme.of(context).primaryColor),
maxLines: 1,
),
), ),
Icon( Icon(
Icons.close_rounded, Icons.close_rounded,

View File

@@ -13,15 +13,14 @@ class AlbumActionOutlinedButton extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: OutlinedButton.icon( child: OutlinedButton.icon(
style: ButtonStyle( style: OutlinedButton.styleFrom(
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(vertical: 0, horizontal: 10)), padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
shape: MaterialStateProperty.resolveWith<OutlinedBorder>( shape: RoundedRectangleBorder(
(_) => RoundedRectangleBorder( borderRadius: BorderRadius.circular(25),
borderRadius: BorderRadius.circular(25),
),
), ),
side: MaterialStateProperty.resolveWith<BorderSide>( side: const BorderSide(
(_) => const BorderSide(width: 1, color: Color.fromARGB(255, 158, 158, 158)), width: 1,
color: Color.fromARGB(255, 215, 215, 215),
), ),
), ),
icon: Icon(iconData, size: 15), icon: Icon(iconData, size: 15),

View File

@@ -49,7 +49,7 @@ class SharingSliverAppBar extends StatelessWidget {
), ),
label: const Text( label: const Text(
"Create shared album", "Create shared album",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
), ),
), ),
), ),
@@ -69,7 +69,7 @@ class SharingSliverAppBar extends StatelessWidget {
), ),
label: const Text( label: const Text(
"Share with partner", "Share with partner",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
), ),
), ),
), ),

View File

@@ -82,11 +82,11 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 16, left: 18, right: 18), padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
child: OutlinedButton.icon( child: OutlinedButton.icon(
style: ButtonStyle( style: OutlinedButton.styleFrom(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(vertical: 22, horizontal: 16)), side: const BorderSide(color: Color.fromARGB(255, 206, 206, 206)),
), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))),
onPressed: _onSelectPhotosButtonPressed, onPressed: _onSelectPhotosButtonPressed,
icon: const Icon(Icons.add_rounded), icon: const Icon(Icons.add_rounded),
label: Padding( label: Padding(

View File

@@ -0,0 +1,53 @@
import 'dart:convert';
class UploadProfileImageResponse {
final String userId;
final String profileImagePath;
UploadProfileImageResponse({
required this.userId,
required this.profileImagePath,
});
UploadProfileImageResponse copyWith({
String? userId,
String? profileImagePath,
}) {
return UploadProfileImageResponse(
userId: userId ?? this.userId,
profileImagePath: profileImagePath ?? this.profileImagePath,
);
}
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
result.addAll({'userId': userId});
result.addAll({'profileImagePath': profileImagePath});
return result;
}
factory UploadProfileImageResponse.fromMap(Map<String, dynamic> map) {
return UploadProfileImageResponse(
userId: map['userId'] ?? '',
profileImagePath: map['profileImagePath'] ?? '',
);
}
String toJson() => json.encode(toMap());
factory UploadProfileImageResponse.fromJson(String source) => UploadProfileImageResponse.fromMap(json.decode(source));
@override
String toString() => 'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UploadProfileImageResponse && other.userId == userId && other.profileImagePath == profileImagePath;
}
@override
int get hashCode => userId.hashCode ^ profileImagePath.hashCode;
}

View File

@@ -0,0 +1,57 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
class ReleaseInfoNotifier extends StateNotifier<String> {
ReleaseInfoNotifier() : super("");
void checkGithubReleaseInfo() async {
var dio = Dio();
var box = Hive.box(hiveGithubReleaseInfoBox);
try {
String? localReleaseVersion = box.get(githubReleaseInfoKey);
Response res = await dio.get(
"https://api.github.com/repos/alextran1502/immich/releases/latest",
options: Options(
headers: {"Accept": "application/vnd.github.v3+json"},
),
);
if (res.statusCode == 200) {
String latestTagVersion = res.data["tag_name"];
state = latestTagVersion;
debugPrint("Local release version $localReleaseVersion");
debugPrint("Remote release veresion $latestTagVersion");
if (localReleaseVersion == null && latestTagVersion.isNotEmpty) {
VersionAnnouncementOverlayController.appLoader.show();
return;
}
if (latestTagVersion.isNotEmpty && localReleaseVersion != latestTagVersion) {
VersionAnnouncementOverlayController.appLoader.show();
return;
}
}
} catch (e) {
debugPrint("Error gettting latest release version");
state = "";
}
}
void acknowledgeNewVersion() {
var box = Hive.box(hiveGithubReleaseInfoBox);
box.put(githubReleaseInfoKey, state);
VersionAnnouncementOverlayController.appLoader.hide();
}
}
final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>((ref) => ReleaseInfoNotifier());

View File

@@ -19,11 +19,6 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
final ServerInfoService _serverInfoService = ServerInfoService(); final ServerInfoService _serverInfoService = ServerInfoService();
getMapboxInfo() async {
MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
state = state.copyWith(mapboxInfo: mapboxInfoRes);
}
getServerVersion() async { getServerVersion() async {
ServerVersion? serverVersion = await _serverInfoService.getServerVersion(); ServerVersion? serverVersion = await _serverInfoService.getServerVersion();

View File

@@ -1,4 +1,5 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/shared/models/mapbox_info.model.dart'; import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
import 'package:immich_mobile/shared/models/server_version.model.dart'; import 'package:immich_mobile/shared/models/server_version.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart'; import 'package:immich_mobile/shared/services/network.service.dart';
@@ -13,15 +14,16 @@ class ServerInfoService {
return ServerInfo.fromJson(response.toString()); return ServerInfo.fromJson(response.toString());
} }
Future<MapboxInfo> getMapboxInfo() async {
Response response = await _networkService.getRequest(url: 'server-info/mapbox');
return MapboxInfo.fromJson(response.toString());
}
Future<ServerVersion?> getServerVersion() async { Future<ServerVersion?> getServerVersion() async {
Response response = await _networkService.getRequest(url: 'server-info/version'); try {
Response response =
await _networkService.getRequest(url: 'server-info/version');
return ServerVersion.fromJson(response.toString()); return ServerVersion.fromJson(response.toString());
} catch (e) {
debugPrint("Error getting server info");
}
return null;
} }
} }

View File

@@ -2,8 +2,15 @@ import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/upload_profile_image_repsonse.model.dart';
import 'package:immich_mobile/shared/models/user_info.model.dart'; import 'package:immich_mobile/shared/models/user_info.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart'; import 'package:immich_mobile/shared/services/network.service.dart';
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
import 'package:immich_mobile/utils/files_helper.dart';
import 'package:http_parser/http_parser.dart';
class UserService { class UserService {
final NetworkService _networkService = NetworkService(); final NetworkService _networkService = NetworkService();
@@ -21,4 +28,39 @@ class UserService {
return []; return [];
} }
Future<UploadProfileImageResponse?> uploadProfileImage(XFile image) async {
var dio = Dio();
dio.interceptors.add(AuthenticatedRequestInterceptor());
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
var mimeType = FileHelper.getMimeType(image.path);
final imageData = MultipartFile.fromBytes(
await image.readAsBytes(),
filename: image.name,
contentType: MediaType(
mimeType["type"],
mimeType["subType"],
),
);
final formData = FormData.fromMap({'file': imageData});
try {
Response res = await dio.post(
'$savedEndpoint/user/profile-image',
data: formData,
);
var payload = UploadProfileImageResponse.fromJson(res.toString());
return payload;
} on DioError catch (e) {
debugPrint("Error uploading file: ${e.response}");
return null;
} catch (e) {
debugPrint("Error uploading file: $e");
return null;
}
}
} }

View File

@@ -19,26 +19,32 @@ class TabControllerPage extends ConsumerWidget {
], ],
builder: (context, child, animation) { builder: (context, child, animation) {
final tabsRouter = AutoTabsRouter.of(context); final tabsRouter = AutoTabsRouter.of(context);
return Scaffold( return WillPopScope(
body: FadeTransition( onWillPop: () async {
opacity: animation, tabsRouter.setActiveIndex(0);
child: child, return false;
},
child: Scaffold(
body: FadeTransition(
opacity: animation,
child: child,
),
bottomNavigationBar: isMultiSelectEnable
? null
: BottomNavigationBar(
selectedLabelStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
unselectedLabelStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
currentIndex: tabsRouter.activeIndex,
onTap: (index) {
tabsRouter.setActiveIndex(index);
},
items: const [
BottomNavigationBarItem(label: 'Photos', icon: Icon(Icons.photo)),
BottomNavigationBarItem(label: 'Search', icon: Icon(Icons.search)),
BottomNavigationBarItem(label: 'Sharing', icon: Icon(Icons.group_outlined)),
],
),
), ),
bottomNavigationBar: isMultiSelectEnable
? null
: BottomNavigationBar(
selectedLabelStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
unselectedLabelStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
currentIndex: tabsRouter.activeIndex,
onTap: (index) {
tabsRouter.setActiveIndex(index);
},
items: const [
BottomNavigationBarItem(label: 'Photos', icon: Icon(Icons.photo)),
BottomNavigationBarItem(label: 'Search', icon: Icon(Icons.search)),
BottomNavigationBarItem(label: 'Sharing', icon: Icon(Icons.group_outlined)),
],
),
); );
}, },
); );

View File

@@ -0,0 +1,133 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
import 'package:url_launcher/url_launcher.dart';
class VersionAnnouncementOverlay extends HookConsumerWidget {
const VersionAnnouncementOverlay({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
void goToReleaseNote() async {
final Uri _url = Uri.parse('https://github.com/alextran1502/immich/releases/latest');
await launchUrl(_url);
}
void onAcknowledgeTapped() {
ref.watch(releaseInfoProvider.notifier).acknowledgeNewVersion();
}
return ValueListenableBuilder<bool>(
valueListenable: VersionAnnouncementOverlayController.appLoader.loaderShowingNotifier,
builder: (context, shouldShow, child) {
if (shouldShow) {
return Scaffold(
backgroundColor: Colors.black38,
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 307),
child: Wrap(
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"New Server Version Available 🎉",
style: TextStyle(
fontSize: 16,
fontFamily: 'WorkSans',
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 14, fontFamily: 'WorkSans', color: Colors.black87, height: 1.2),
children: <TextSpan>[
const TextSpan(
text: 'Hi friend, there is a new release of',
),
const TextSpan(
text: ' Immich ',
style: TextStyle(
fontFamily: "SnowBurstOne",
color: Colors.indigo,
fontWeight: FontWeight.bold,
),
),
const TextSpan(
text: "please take your time to visit the ",
),
TextSpan(
text: "release note",
style: const TextStyle(
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = goToReleaseNote,
),
const TextSpan(
text:
" and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
)
],
),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
visualDensity: VisualDensity.standard,
primary: Colors.indigo,
onPrimary: Colors.grey[50],
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
),
onPressed: onAcknowledgeTapped,
child: const Text(
"Acknowledge",
style: TextStyle(
fontSize: 14,
),
)),
)
],
),
),
),
],
),
),
),
);
} else {
return Container();
}
},
);
}
}
class VersionAnnouncementOverlayController {
static final VersionAnnouncementOverlayController appLoader = VersionAnnouncementOverlayController();
ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false);
ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
void show() {
loaderShowingNotifier.value = true;
}
void hide() {
loaderShowingNotifier.value = false;
}
}

View File

@@ -7,28 +7,28 @@ packages:
name: _fe_analyzer_shared name: _fe_analyzer_shared
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "34.0.0" version: "38.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" version: "3.4.1"
archive: archive:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.11" version: "3.3.0"
args: args:
dependency: transitive dependency: transitive
description: description:
name: args name: args
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -42,14 +42,14 @@ packages:
name: auto_route name: auto_route
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.2" version: "4.0.1"
auto_route_generator: auto_route_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: auto_route_generator name: auto_route_generator
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.1" version: "4.0.0"
badges: badges:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -70,7 +70,7 @@ packages:
name: build name: build
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
build_config: build_config:
dependency: transitive dependency: transitive
description: description:
@@ -84,21 +84,21 @@ packages:
name: build_daemon name: build_daemon
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.1.0"
build_resolvers: build_resolvers:
dependency: transitive dependency: transitive
description: description:
name: build_resolvers name: build_resolvers
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.6" version: "2.0.8"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.7" version: "2.1.10"
build_runner_core: build_runner_core:
dependency: transitive dependency: transitive
description: description:
@@ -119,14 +119,14 @@ packages:
name: built_value name: built_value
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.1.4" version: "8.3.0"
cached_network_image: cached_network_image:
dependency: "direct main" dependency: "direct main"
description: description:
name: cached_network_image name: cached_network_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" version: "3.2.1"
cached_network_image_platform_interface: cached_network_image_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -168,14 +168,7 @@ packages:
name: chewie name: chewie
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.2" version: "1.3.2"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -196,7 +189,7 @@ packages:
name: collection name: collection
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.15.0" version: "1.16.0"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@@ -204,13 +197,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3+1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
name: crypto name: crypto
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@@ -231,14 +231,14 @@ packages:
name: dart_style name: dart_style
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.1" version: "2.2.3"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.4" version: "4.0.6"
equatable: equatable:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -259,14 +259,14 @@ packages:
name: fake_async name: fake_async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.2" version: "1.2.1"
file: file:
dependency: transitive dependency: transitive
description: description:
@@ -280,7 +280,7 @@ packages:
name: fixnum name: fixnum
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -292,7 +292,7 @@ packages:
name: flutter_blurhash name: flutter_blurhash
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.0" version: "0.6.8"
flutter_cache_manager: flutter_cache_manager:
dependency: transitive dependency: transitive
description: description:
@@ -306,7 +306,7 @@ packages:
name: flutter_hooks name: flutter_hooks
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.18.2" version: "0.18.4"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -328,13 +328,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.14.0" version: "0.14.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
flutter_riverpod: flutter_riverpod:
dependency: transitive dependency: transitive
description: description:
name: flutter_riverpod name: flutter_riverpod
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0-dev.0" version: "2.0.0-dev.7"
flutter_spinkit: flutter_spinkit:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -372,7 +379,7 @@ packages:
name: fluttertoast name: fluttertoast
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.0.8" version: "8.0.9"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@@ -400,7 +407,7 @@ packages:
name: hive name: hive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.2.1"
hive_flutter: hive_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -421,7 +428,7 @@ packages:
name: hooks_riverpod name: hooks_riverpod
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0-dev.0" version: "2.0.0-dev.7"
html: html:
dependency: transitive dependency: transitive
description: description:
@@ -442,7 +449,7 @@ packages:
name: http_multi_server name: http_multi_server
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.2.0"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@@ -456,7 +463,42 @@ packages:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.1" version: "3.1.3"
image_picker:
dependency: "direct main"
description:
name: image_picker
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.5+3"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.4+13"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.8"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.5+5"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -477,14 +519,14 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.4"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
name: json_annotation name: json_annotation
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.4.0" version: "4.5.0"
latlong2: latlong2:
dependency: transitive dependency: transitive
description: description:
@@ -526,7 +568,7 @@ packages:
name: material_color_utilities name: material_color_utilities
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.3" version: "0.1.4"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -547,7 +589,7 @@ packages:
name: mime name: mime
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.2"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -575,14 +617,14 @@ packages:
name: package_info_plus name: package_info_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.0" version: "1.4.2"
package_info_plus_linux: package_info_plus_linux:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_linux name: package_info_plus_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.0.5"
package_info_plus_macos: package_info_plus_macos:
dependency: transitive dependency: transitive
description: description:
@@ -603,70 +645,70 @@ packages:
name: package_info_plus_web name: package_info_plus_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
package_info_plus_windows: package_info_plus_windows:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_windows name: package_info_plus_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
path: path:
dependency: transitive dependency: transitive
description: description:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.1"
path_provider: path_provider:
dependency: transitive dependency: transitive
description: description:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.8" version: "2.0.10"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.11" version: "2.0.14"
path_provider_ios: path_provider_ios:
dependency: transitive dependency: transitive
description: description:
name: path_provider_ios name: path_provider_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.7" version: "2.0.9"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.5" version: "2.1.6"
path_provider_macos: path_provider_macos:
dependency: transitive dependency: transitive
description: description:
name: path_provider_macos name: path_provider_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.6"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.6"
pedantic: pedantic:
dependency: transitive dependency: transitive
description: description:
@@ -680,21 +722,21 @@ packages:
name: percent_indicator name: percent_indicator
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.4.0" version: "4.2.2"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.4.0" version: "5.0.0"
photo_manager: photo_manager:
dependency: "direct main" dependency: "direct main"
description: description:
name: photo_manager name: photo_manager
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.6" version: "2.1.0+2"
photo_view: photo_view:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -750,14 +792,14 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.0" version: "6.0.2"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
name: pub_semver name: pub_semver
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
pubspec_parse: pubspec_parse:
dependency: transitive dependency: transitive
description: description:
@@ -771,14 +813,14 @@ packages:
name: quiver name: quiver
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1+1" version: "3.1.0"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:
name: riverpod name: riverpod
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0-dev.0" version: "2.0.0-dev.7"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@@ -792,7 +834,7 @@ packages:
name: shelf name: shelf
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
shelf_web_socket: shelf_web_socket:
dependency: transitive dependency: transitive
description: description:
@@ -811,7 +853,7 @@ packages:
name: sliver_tools name: sliver_tools
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.5" version: "0.2.6"
socket_io_client: socket_io_client:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -832,21 +874,21 @@ packages:
name: source_gen name: source_gen
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
source_helper: source_helper:
dependency: transitive dependency: transitive
description: description:
name: source_helper name: source_helper
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.2"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.2"
sprintf: sprintf:
dependency: transitive dependency: transitive
description: description:
@@ -860,14 +902,14 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2+1"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1+1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -909,7 +951,7 @@ packages:
name: synchronized name: synchronized
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0+2"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -923,7 +965,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.8" version: "0.4.9"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@@ -951,7 +993,7 @@ packages:
name: typed_data name: typed_data
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
unicode: unicode:
dependency: transitive dependency: transitive
description: description:
@@ -973,55 +1015,118 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.4"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.3"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
name: uuid name: uuid
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.5" version: "3.0.6"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
very_good_analysis:
dependency: transitive
description:
name: very_good_analysis
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
video_player: video_player:
dependency: "direct main" dependency: "direct main"
description: description:
name: video_player name: video_player
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.18" version: "2.4.2"
video_player_android: video_player_android:
dependency: transitive dependency: transitive
description: description:
name: video_player_android name: video_player_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.17" version: "2.3.3"
video_player_avfoundation: video_player_avfoundation:
dependency: transitive dependency: transitive
description: description:
name: video_player_avfoundation name: video_player_avfoundation
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.18" version: "2.3.4"
video_player_platform_interface: video_player_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: video_player_platform_interface name: video_player_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.1" version: "5.1.2"
video_player_web: video_player_web:
dependency: transitive dependency: transitive
description: description:
name: video_player_web name: video_player_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.6" version: "2.0.10"
visibility_detector: visibility_detector:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1035,7 +1140,7 @@ packages:
name: wakelock name: wakelock
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.6" version: "0.6.1+2"
wakelock_macos: wakelock_macos:
dependency: transitive dependency: transitive
description: description:
@@ -1077,14 +1182,14 @@ packages:
name: web_socket_channel name: web_socket_channel
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.2.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.3.8" version: "2.5.2"
wkt_parser: wkt_parser:
dependency: transitive dependency: transitive
description: description:
@@ -1098,21 +1203,21 @@ packages:
name: xdg_directories name: xdg_directories
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0+1"
xml: xml:
dependency: transitive dependency: transitive
description: description:
name: xml name: xml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.3.1" version: "5.4.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
name: yaml name: yaml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
sdks: sdks:
dart: ">=2.15.1 <3.0.0" dart: ">=2.17.0 <3.0.0"
flutter: ">=2.8.0" flutter: ">=3.0.0"

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: "none" publish_to: "none"
version: 1.9.0+13 version: 1.11.0+17
environment: environment:
sdk: ">=2.15.1 <3.0.0" sdk: ">=2.15.1 <3.0.0"
@@ -14,13 +14,13 @@ dependencies:
photo_manager: ^2.0.6 photo_manager: ^2.0.6
flutter_hooks: ^0.18.0 flutter_hooks: ^0.18.0
hooks_riverpod: ^2.0.0-dev.0 hooks_riverpod: ^2.0.0-dev.0
hive: hive: ^2.2.1
hive_flutter: hive_flutter: ^1.1.0
dio: ^4.0.4 dio: ^4.0.4
cached_network_image: ^3.2.0 cached_network_image: ^3.2.1
percent_indicator: ^3.4.0 percent_indicator: ^4.2.2
intl: ^0.17.0 intl: ^0.17.0
auto_route: ^3.2.2 auto_route: ^4.0.1
exif: ^3.1.1 exif: ^3.1.1
transparent_image: ^2.0.0 transparent_image: ^2.0.0
visibility_detector: ^0.2.2 visibility_detector: ^0.2.2
@@ -38,6 +38,8 @@ dependencies:
flutter_spinkit: ^5.1.0 flutter_spinkit: ^5.1.0
flutter_swipe_detector: ^2.0.0 flutter_swipe_detector: ^2.0.0
equatable: ^2.0.3 equatable: ^2.0.3
image_picker: ^0.8.5+3
url_launcher: ^6.1.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -45,7 +47,7 @@ dev_dependencies:
flutter_lints: ^1.0.0 flutter_lints: ^1.0.0
hive_generator: ^1.1.2 hive_generator: ^1.1.2
build_runner: ^2.1.7 build_runner: ^2.1.7
auto_route_generator: ^3.2.1 auto_route_generator: ^4.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@@ -1,5 +1,6 @@
{ {
"singleQuote": true, "singleQuote": true,
"trailingComma": "all", "trailingComma": "all",
"printWidth": 120 "printWidth": 120,
"semi": true
} }

View File

@@ -1,4 +1,4 @@
FROM node:16-alpine3.14 FROM node:16-alpine3.14 as core
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
@@ -6,10 +6,10 @@ WORKDIR /usr/src/app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN apk add --update-cache build-base python3 RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
RUN npm install RUN npm install --legacy-peer-deps
COPY . . COPY . .
RUN npm run build RUN npm run build

View File

@@ -19,11 +19,11 @@ import {
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AssetService } from './asset.service'; import { AssetService } from './asset.service';
import { FileFieldsInterceptor } from '@nestjs/platform-express'; import { FileFieldsInterceptor } from '@nestjs/platform-express';
import { multerOption } from '../../config/multer-option.config'; import { assetUploadOption } from '../../config/asset-upload.config';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto'; import { CreateAssetDto } from './dto/create-asset.dto';
import { ServeFileDto } from './dto/serve-file.dto'; import { ServeFileDto } from './dto/serve-file.dto';
import { AssetEntity } from './entities/asset.entity'; import { AssetEntity } from '@app/database/entities/asset.entity';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { Response as Res } from 'express'; import { Response as Res } from 'express';
import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto'; import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
@@ -31,6 +31,8 @@ import { BackgroundTaskService } from '../../modules/background-task/background-
import { DeleteAssetDto } from './dto/delete-asset.dto'; import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto'; import { SearchAssetDto } from './dto/search-asset.dto';
import { CommunicationGateway } from '../communication/communication.gateway'; import { CommunicationGateway } from '../communication/communication.gateway';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('asset') @Controller('asset')
@@ -39,6 +41,9 @@ export class AssetController {
private wsCommunicateionGateway: CommunicationGateway, private wsCommunicateionGateway: CommunicationGateway,
private assetService: AssetService, private assetService: AssetService,
private backgroundTaskService: BackgroundTaskService, private backgroundTaskService: BackgroundTaskService,
@InjectQueue('asset-uploaded-queue')
private assetUploadedQueue: Queue,
) {} ) {}
@Post('upload') @Post('upload')
@@ -48,7 +53,7 @@ export class AssetController {
{ name: 'assetData', maxCount: 1 }, { name: 'assetData', maxCount: 1 },
{ name: 'thumbnailData', maxCount: 1 }, { name: 'thumbnailData', maxCount: 1 },
], ],
multerOption, assetUploadOption,
), ),
) )
async uploadFile( async uploadFile(
@@ -61,12 +66,23 @@ export class AssetController {
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype); const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
if (uploadFiles.thumbnailData != null && savedAsset) { if (uploadFiles.thumbnailData != null && savedAsset) {
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path); const assetWithThumbnail = await this.assetService.updateThumbnailInfo(
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset); savedAsset,
await this.backgroundTaskService.detectObject(uploadFiles.thumbnailData[0].path, savedAsset); uploadFiles.thumbnailData[0].path,
} );
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size); await this.assetUploadedQueue.add(
'asset-uploaded',
{ asset: assetWithThumbnail, fileName: file.originalname, fileSize: file.size, hasThumbnail: true },
{ jobId: savedAsset.id },
);
} else {
await this.assetUploadedQueue.add(
'asset-uploaded',
{ asset: savedAsset, fileName: file.originalname, fileSize: file.size, hasThumbnail: false },
{ jobId: savedAsset.id },
);
}
this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset)); this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
} catch (e) { } catch (e) {
@@ -123,7 +139,7 @@ export class AssetController {
@Get('/') @Get('/')
async getAllAssets(@GetAuthUser() authUser: AuthUserDto) { async getAllAssets(@GetAuthUser() authUser: AuthUserDto) {
return await this.assetService.getAllAssetsNoPagination(authUser); return await this.assetService.getAllAssets(authUser);
} }
@Get('/:deviceId') @Get('/:deviceId')

View File

@@ -2,9 +2,7 @@ import { Module } from '@nestjs/common';
import { AssetService } from './asset.service'; import { AssetService } from './asset.service';
import { AssetController } from './asset.controller'; import { AssetController } from './asset.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from './entities/asset.entity'; import { AssetEntity } from '@app/database/entities/asset.entity';
import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module';
import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module'; import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
@@ -13,29 +11,19 @@ import { CommunicationModule } from '../communication/communication.module';
@Module({ @Module({
imports: [ imports: [
CommunicationModule, CommunicationModule,
BullModule.registerQueue({
name: 'optimize',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
BullModule.registerQueue({
name: 'background-task',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
TypeOrmModule.forFeature([AssetEntity]),
ImageOptimizeModule,
BackgroundTaskModule, BackgroundTaskModule,
TypeOrmModule.forFeature([AssetEntity]),
BullModule.registerQueue({
name: 'asset-uploaded-queue',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
], ],
controllers: [AssetController], controllers: [AssetController],
providers: [AssetService, AssetOptimizeService, BackgroundTaskService], providers: [AssetService, BackgroundTaskService],
exports: [], exports: [],
}) })
export class AssetModule {} export class AssetModule {}

View File

@@ -1,19 +1,16 @@
import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common'; import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto'; import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetEntity, AssetType } from './entities/asset.entity'; import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import _ from 'lodash'; import _ from 'lodash';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
import { createReadStream, stat } from 'fs'; import { createReadStream, stat } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto'; import { ServeFileDto } from './dto/serve-file.dto';
import { Response as Res } from 'express'; import { Response as Res } from 'express';
import { promisify } from 'util'; import { promisify } from 'util';
import { DeleteAssetDto } from './dto/delete-asset.dto'; import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto'; import { SearchAssetDto } from './dto/search-asset.dto';
import path from 'path';
const fileInfo = promisify(stat); const fileInfo = promisify(stat);
@@ -24,10 +21,16 @@ export class AssetService {
private assetRepository: Repository<AssetEntity>, private assetRepository: Repository<AssetEntity>,
) {} ) {}
public async updateThumbnailInfo(assetId: string, path: string) { public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise<AssetEntity> {
return await this.assetRepository.update(assetId, { const updatedAsset = await this.assetRepository
resizePath: path, .createQueryBuilder('assets')
}); .update<AssetEntity>(AssetEntity, { ...asset, resizePath: thumbnailPath })
.where('assets.id = :id', { id: asset.id })
.returning('*')
.updateEntity(true)
.execute();
return updatedAsset.raw[0];
} }
public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) { public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
@@ -64,13 +67,17 @@ export class AssetService {
return res; return res;
} }
public async getAllAssetsNoPagination(authUser: AuthUserDto) { public async getAllAssets(authUser: AuthUserDto) {
try { try {
return await this.assetRepository return await this.assetRepository.find({
.createQueryBuilder('a') where: {
.where('a."userId" = :userId', { userId: authUser.id }) userId: authUser.id,
.orderBy('a."createdAt"::date', 'DESC') },
.getMany(); relations: ['exifInfo'],
order: {
createdAt: 'DESC',
},
});
} catch (e) { } catch (e) {
Logger.error(e, 'getAllAssets'); Logger.error(e, 'getAllAssets');
} }
@@ -99,96 +106,164 @@ export class AssetService {
} }
public async downloadFile(query: ServeFileDto, res: Res) { public async downloadFile(query: ServeFileDto, res: Res) {
let file = null; try {
const asset = await this.findOne(query.did, query.aid); let file = null;
const asset = await this.findOne(query.did, query.aid);
if (query.isThumb === 'false' || !query.isThumb) { if (query.isThumb === 'false' || !query.isThumb) {
file = createReadStream(asset.originalPath); const { size } = await fileInfo(asset.originalPath);
} else { res.set({
file = createReadStream(asset.resizePath); 'Content-Type': asset.mimeType,
'Content-Length': size,
});
file = createReadStream(asset.originalPath);
} else {
const { size } = await fileInfo(asset.resizePath);
res.set({
'Content-Type': 'image/jpeg',
'Content-Length': size,
});
file = createReadStream(asset.resizePath);
}
return new StreamableFile(file);
} catch (e) {
Logger.error('Error download asset ', e);
throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
} }
return new StreamableFile(file);
} }
public async getAssetThumbnail(assetId: string) { public async getAssetThumbnail(assetId: string) {
const asset = await this.assetRepository.findOne({ id: assetId }); try {
const asset = await this.assetRepository.findOne({ id: assetId });
return new StreamableFile(createReadStream(asset.resizePath)); if (asset.webpPath && asset.webpPath.length > 0) {
return new StreamableFile(createReadStream(asset.webpPath));
} else {
return new StreamableFile(createReadStream(asset.resizePath));
}
} catch (e) {
Logger.error('Error serving asset thumbnail ', e);
throw new InternalServerErrorException('Failed to serve asset thumbnail', 'GetAssetThumbnail');
}
} }
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) { public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
let file = null; let file = null;
const asset = await this.findOne(query.did, query.aid); const asset = await this.findOne(query.did, query.aid);
if (!asset) { if (!asset) {
throw new BadRequestException('Asset does not exist'); throw new BadRequestException('Asset does not exist');
} }
// Handle Sending Images // Handle Sending Images
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') { if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
res.set({ /**
'Content-Type': asset.mimeType, * Serve file viewer on the web
}); */
if (query.isWeb) {
if (query.isThumb === 'false' || !query.isThumb) { res.set({
file = createReadStream(asset.originalPath); 'Content-Type': 'image/jpeg',
} else { });
file = createReadStream(asset.resizePath); return new StreamableFile(createReadStream(asset.resizePath));
} }
file.on('error', (error) => { try {
Logger.log(`Cannot create read stream ${error}`); /**
return new BadRequestException('Cannot Create Read Stream'); * Serve thumbnail image for both web and mobile app
}); */
return new StreamableFile(file); if (query.isThumb === 'false' || !query.isThumb) {
res.set({
'Content-Type': asset.mimeType,
});
file = createReadStream(asset.originalPath);
} else {
if (asset.webpPath && asset.webpPath.length > 0) {
res.set({
'Content-Type': 'image/webp',
});
file = createReadStream(asset.webpPath);
} else {
res.set({
'Content-Type': 'image/jpeg',
});
file = createReadStream(asset.resizePath);
}
}
file.on('error', (error) => {
Logger.log(`Cannot create read stream ${error}`);
return new BadRequestException('Cannot Create Read Stream');
});
return new StreamableFile(file);
} catch (e) {
Logger.error('Error serving IMAGE asset ', e);
throw new InternalServerErrorException(`Failed to serve image asset ${e}`, 'ServeFile');
}
} else if (asset.type == AssetType.VIDEO) { } else if (asset.type == AssetType.VIDEO) {
// Handle Handling Video try {
const { size } = await fileInfo(asset.originalPath); // Handle Video
const range = headers.range; let videoPath = asset.originalPath;
let mimeType = asset.mimeType;
if (range) { if (query.isWeb && asset.mimeType == 'video/quicktime') {
/** Extracting Start and End value from Range Header */ videoPath = asset.encodedVideoPath == '' ? asset.originalPath : asset.encodedVideoPath;
let [start, end] = range.replace(/bytes=/, '').split('-'); mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4';
start = parseInt(start, 10);
end = end ? parseInt(end, 10) : size - 1;
if (!isNaN(start) && isNaN(end)) {
start = start;
end = size - 1;
}
if (isNaN(start) && !isNaN(end)) {
start = size - end;
end = size - 1;
} }
// Handle unavailable range request const { size } = await fileInfo(videoPath);
if (start >= size || end >= size) { const range = headers.range;
console.error('Bad Request');
// Return the 416 Range Not Satisfiable. if (range) {
res.status(416).set({ /** Extracting Start and End value from Range Header */
'Content-Range': `bytes */${size}`, let [start, end] = range.replace(/bytes=/, '').split('-');
start = parseInt(start, 10);
end = end ? parseInt(end, 10) : size - 1;
if (!isNaN(start) && isNaN(end)) {
start = start;
end = size - 1;
}
if (isNaN(start) && !isNaN(end)) {
start = size - end;
end = size - 1;
}
// Handle unavailable range request
if (start >= size || end >= size) {
console.error('Bad Request');
// Return the 416 Range Not Satisfiable.
res.status(416).set({
'Content-Range': `bytes */${size}`,
});
throw new BadRequestException('Bad Request Range');
}
/** Sending Partial Content With HTTP Code 206 */
res.status(206).set({
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
'Content-Type': mimeType,
}); });
throw new BadRequestException('Bad Request Range'); const videoStream = createReadStream(videoPath, { start: start, end: end });
return new StreamableFile(videoStream);
} else {
res.set({
'Content-Type': mimeType,
});
return new StreamableFile(createReadStream(videoPath));
} }
} catch (e) {
/** Sending Partial Content With HTTP Code 206 */ Logger.error('Error serving VIDEO asset ', e);
throw new InternalServerErrorException(`Failed to serve video asset ${e}`, 'ServeFile');
res.status(206).set({
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
'Content-Type': asset.mimeType,
});
const videoStream = createReadStream(asset.originalPath, { start: start, end: end });
return new StreamableFile(videoStream);
} else {
res.set({
'Content-Type': asset.mimeType,
});
return new StreamableFile(createReadStream(asset.originalPath));
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import { IsNotEmpty, IsOptional } from 'class-validator'; import { IsNotEmpty, IsOptional } from 'class-validator';
import { AssetType } from '../entities/asset.entity'; import { AssetType } from '@app/database/entities/asset.entity';
export class CreateAssetDto { export class CreateAssetDto {
@IsNotEmpty() @IsNotEmpty()

Some files were not shown because too many files have changed in this diff Show More