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
+13 -2
View File
@@ -1,15 +1,26 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
title: '[BUG] <title>'
labels: bug, need triage
assignees: ''
---
<!--
Note: Please search to see if an issue already exists for the bug you encountered.
-->
**Describe the bug**
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**
Steps to reproduce the behavior:
1. Go to '...'
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
@@ -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
+62 -33
View File
@@ -4,18 +4,18 @@ on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build_and_push_server_latest:
# 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_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main" # branch
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
@@ -26,39 +26,68 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich
- 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' }}
push: true
tags: |
altran1502/immich-server:latest
build_and_push_microservice_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main" # branch
- 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 Microservices
uses: docker/build-push-action@v3.0.0
with:
context: ./microservices
file: ./microservices/Dockerfile
platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: |
altran1502/immich-microservices:latest
build_and_push_machine_learning_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 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: true
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
@@ -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
@@ -12,14 +12,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main"
ref: "main"
fetch-depth: 0
- name: 'Get Previous tag'
- name: "Get Previous tag"
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: latest
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
@@ -50,14 +50,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main"
ref: "main"
fetch-depth: 0
- name: 'Get Previous tag'
- name: "Get Previous tag"
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: latest
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
@@ -80,4 +80,43 @@ jobs:
platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name != 'pull_request' }}
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 }}
+7 -1
View File
@@ -5,7 +5,13 @@ dev-update:
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
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:
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
+9
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
+67 -27
View File
@@ -31,6 +31,7 @@ Loading ~4000 images/videos
## Screenshots
### Mobile client
<p align="left">
<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">
@@ -39,14 +40,18 @@ Loading ~4000 images/videos
<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/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>
# Note
**!! 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
@@ -67,29 +72,30 @@ This project is under heavy development, there will be continous functions, feat
- Show curated objects on the search page
- Shared album with users on the same server
- Selective backup - albums can be included and excluded during the backup process.
- Web interface is available for administrative tasks (creating new users) and viewing assets on the server - additional features are coming.
# System Requirement
**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
*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.
**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:
1. **NestJs** - Backend of the application
2. **PostgreSQL** - Main database of the application
3. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
4. **Nginx** - Load balancing and optimized file uploading.
5. **TensorFlow** - Object Detection and Image Classification.
2. **SvelteKit** - Web frontend of the application
3. **PostgreSQL** - Main database of the application
4. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
5. **Nginx** - Load balancing and optimized file uploading.
6. **TensorFlow** - Object Detection and Image Classification.
## Step 1: Populate .env file
@@ -108,52 +114,75 @@ Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is own
**Example**
```bash
###################################################################################
# Database
###################################################################################
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=immich
###################################################################################
# Upload File Config
###################################################################################
UPLOAD_LOCATION=<put-the-path-of-the-upload-folder-here>
###################################################################################
# JWT SECRET
###################################################################################
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
###################################################################################
# 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
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
To start, run
To **start**, run
```bash
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
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`
## 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
curl --location --request POST 'http://your-server-ip:2283/auth/signUp' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "testuser@email.com",
"password": "password"
}'
```
<p align="left">
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
<p/>
Additional accounts on the server can be created by the admin account.
<p align="left">
<img src="design/admin-interface.png" width="500" title="Admin User Management">
<p/>
## 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">
<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
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)
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
@@ -204,13 +244,13 @@ Cheer! 🎉
*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
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.
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

+50 -2
View File
@@ -1,15 +1,63 @@
###################################################################################
# Database
###################################################################################
DB_HOSTNAME=immich_postgres
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=immich
###################################################################################
# Redis
###################################################################################
REDIS_HOSTNAME=immich_redis
###################################################################################
# Upload File Config
###################################################################################
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
###################################################################################
# JWT SECRET
###################################################################################
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
###################################################################################
# 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
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
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
+50 -13
View File
@@ -1,12 +1,12 @@
version: "3.8"
services:
immich_server:
immich-server:
image: immich-server-dev:1.9.0
build:
context: ../server
dockerfile: Dockerfile
command: npm run start:dev
command: npm run start:dev immich
expose:
- "3000"
volumes:
@@ -21,18 +21,18 @@ services:
- redis
- database
networks:
- immich_network
- immich-network
immich_microservices:
image: immich-microservices-dev:1.9.0
immich-machine-learning:
image: immich-machine-learning-dev:1.9.0
build:
context: ../microservices
context: ../machine-learning
dockerfile: Dockerfile
command: npm run start:dev
expose:
- "3001"
volumes:
- ../microservices:/usr/src/app
- ../machine-learning:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
env_file:
@@ -42,14 +42,51 @@ services:
depends_on:
- database
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:
container_name: immich_redis
image: redis:6.2
networks:
- immich_network
- immich-network
database:
container_name: immich_postgres
@@ -66,7 +103,7 @@ services:
ports:
- 5432:5432
networks:
- immich_network
- immich-network
nginx:
container_name: proxy_nginx
@@ -79,11 +116,11 @@ services:
logging:
driver: none
networks:
- immich_network
- immich-network
depends_on:
- immich_server
- immich-server
networks:
immich_network:
immich-network:
volumes:
pgdata:
+9 -9
View File
@@ -1,7 +1,7 @@
version: "3.8"
services:
immich_server:
immich-server:
image: immich-server-dev:1.9.0
build:
context: ../server
@@ -19,9 +19,9 @@ services:
- redis
- database
networks:
- immich_network
- immich-network
immich_microservices:
immich-microservices:
image: immich-microservices-dev:1.9.0
build:
context: ../microservices
@@ -46,13 +46,13 @@ services:
- database
- immich_server
networks:
- immich_network
- immich-network
redis:
container_name: immich_redis
image: redis:6.2
networks:
- immich_network
- immich-network
database:
container_name: immich_postgres
@@ -69,7 +69,7 @@ services:
ports:
- 5432:5432
networks:
- immich_network
- immich-network
nginx:
container_name: proxy_nginx
@@ -82,11 +82,11 @@ services:
logging:
driver: none
networks:
- immich_network
- immich-network
depends_on:
- immich_server
- immich-server
networks:
immich_network:
immich-network:
volumes:
pgdata:
+110
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:
+50
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:
+45 -15
View File
@@ -1,9 +1,9 @@
version: "3.8"
services:
immich_server:
image: altran1502/immich-server:v1.8.0_12-dev
entrypoint: ["/bin/sh", "./entrypoint.sh"]
immich-server:
image: altran1502/immich-server:latest
entrypoint: ["/bin/sh", "./start-server.sh"]
expose:
- "3000"
volumes:
@@ -16,11 +16,27 @@ services:
- redis
- database
networks:
- immich_network
restart: unless-stopped
- immich-network
restart: always
immich_microservices:
image: altran1502/immich-microservices:v1.8.0_12-dev
immich-microservices:
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"]
expose:
- "3001"
@@ -33,14 +49,26 @@ services:
depends_on:
- database
networks:
- immich_network
restart: unless-stopped
- immich-network
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:
container_name: immich_redis
image: redis:6.2
networks:
- immich_network
- immich-network
restart: always
database:
container_name: immich_postgres
@@ -57,7 +85,8 @@ services:
ports:
- 5432:5432
networks:
- immich_network
- immich-network
restart: always
nginx:
container_name: proxy_nginx
@@ -70,11 +99,12 @@ services:
logging:
driver: none
networks:
- immich_network
- immich-network
depends_on:
- immich_server
- immich-server
restart: always
networks:
immich_network:
immich-network:
volumes:
pgdata:
pgdata:
+1 -1
View File
@@ -41,6 +41,6 @@ server {
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_pass http://immich_server:3000;
proxy_pass http://immich-server:3000;
}
}
@@ -32,4 +32,6 @@ lerna-debug.log*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/extensions.json
upload/
@@ -2,7 +2,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export const databaseConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: 'immich_postgres',
host: process.env.DB_HOSTNAME || 'immich_postgres',
port: 5432,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
@@ -5,9 +5,9 @@ import { ImageClassifierService } from './image-classifier.service';
export class ImageClassifierController {
constructor(
private readonly imageClassifierService: ImageClassifierService,
) {}
) { }
@Post('/tagImage')
@Post('/tag-image')
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
return await this.imageClassifierService.tagImage(thumbnailPath);
}
@@ -8,14 +8,14 @@ async function bootstrap() {
await app.listen(3001, () => {
if (process.env.NODE_ENV == 'development') {
Logger.log(
'Running Immich Microservices in DEVELOPMENT environment',
'Running Immich Machine Learning in DEVELOPMENT environment',
'IMMICH MICROSERVICES',
);
}
if (process.env.NODE_ENV == 'production') {
Logger.log(
'Running Immich Microservices in PRODUCTION environment',
'Running Immich Machine Learning in PRODUCTION environment',
'IMMICH MICROSERVICES',
);
}
@@ -1,13 +1,14 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ObjectDetectionService } from './object-detection.service';
import { Logger } from '@nestjs/common';
@Controller('object-detection')
export class ObjectDetectionController {
constructor(
private readonly objectDetectionService: ObjectDetectionService,
) {}
) { }
@Post('/detectObject')
@Post('/detect-object')
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
return await this.objectDetectionService.detectObject(thumbnailPath);
}
-1
View File
@@ -1 +0,0 @@
devenv/
-3
View File
@@ -1,3 +0,0 @@
__pycache__/
devenv/
app/upload
-25
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
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

@@ -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
-46
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",
# )
@@ -1,4 +0,0 @@
def run_detection():
print("run detection")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

-8
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
@@ -1,5 +1,5 @@
<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">
<!-- Specifies an Android theme to apply to this Activity as soon as
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.WRITE_EXTERNAL_STORAGE" />
<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>
@@ -1,16 +1,21 @@
// Generated file.
//
// If you wish to remove Flutter's multidex support, delete this entire file.
//
// Modifications to this file should be done in a copy under a different name
// as this file may be regenerated.
package io.flutter.app;
import android.app.Application;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex;
/**
* Extension of {@link 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
@CallSuper
protected void attachBaseContext(Context base) {
@@ -0,0 +1 @@
* Hotfix: Permission is being requested now when open backup screen on Android10
@@ -0,0 +1 @@
* User can now upload profile picture from the home page control drawer.
@@ -0,0 +1,2 @@
* Update to Material Design 3
* Fixed back button navigation - no longer return back to the home page
@@ -0,0 +1 @@
* Added announcement pop-up when a new released is pushed out in Github.
+8 -2
View File
@@ -9,6 +9,8 @@ PODS:
- FMDB (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):
- Flutter
- path_provider_ios (0.0.1):
@@ -30,6 +32,7 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/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`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
@@ -50,6 +53,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_udid/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios:
@@ -66,10 +71,11 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
+3 -3
View File
@@ -360,7 +360,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -495,7 +495,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -522,7 +522,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
+17 -3
View File
@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<string>1.10.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>2</string>
<string>14</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
@@ -43,6 +43,12 @@
<key>NSPhotoLibraryAddUsageDescription</key>
<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>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -52,7 +58,7 @@
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@@ -68,5 +74,13 @@
<true />
<key>ITSAppUsesNonExemptEncryption</key>
<false />
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
</dict>
</plist>
+1 -1
View File
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.9.0"
version_number: "1.11.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
+1 -1
View File
@@ -21,7 +21,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
[bundle exec] fastlane ios beta
```
iOS deployment
iOS Beta
----
+2 -17
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 classname="fastlane.lanes" name="1: latest_testflight_build_number" time="4.608292">
</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 classname="fastlane.lanes" name="1: increment_version_number" time="16.3225">
</testcase>
+4
View File
@@ -13,3 +13,7 @@ const String savedLoginInfoKey = "immichSavedLoginInfoKey";
// Backup Info
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox";
const String backupInfoKey = "immichBackupAlbumInfoKey";
// Github Release Info
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox";
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey";
+21 -5
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/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/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/release_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/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'constants/hive_box.dart';
void main() async {
@@ -24,6 +27,7 @@ void main() async {
await Hive.openBox(userInfoBox);
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
await Hive.openBox(hiveGithubReleaseInfoBox);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
@@ -48,10 +52,18 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
case AppLifecycleState.resumed:
debugPrint("[APP STATE] 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(assetProvider.notifier).getAllAsset();
ref.watch(serverInfoProvider.notifier).getServerVersion();
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
break;
@@ -76,7 +88,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
}
Future<void> initApp() async {
WidgetsBinding.instance?.addObserver(this);
WidgetsBinding.instance.addObserver(this);
}
@override
@@ -87,7 +99,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
@override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -95,6 +107,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
@override
Widget build(BuildContext context) {
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Stack(
@@ -103,6 +117,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
title: 'Immich',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primarySwatch: Colors.indigo,
fontFamily: 'WorkSans',
@@ -120,6 +135,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
),
const ImmichLoadingOverlay(),
const VersionAnnouncementOverlay(),
],
),
);
@@ -38,5 +38,7 @@ class HiveBackupAlbumsAdapter extends TypeAdapter<HiveBackupAlbums> {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HiveBackupAlbumsAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
other is HiveBackupAlbumsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
@@ -73,7 +73,7 @@ class BackupService {
});
// 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) {
thumbnailUploadData = MultipartFile.fromBytes(
List.from(thumbnailData),
@@ -45,12 +45,16 @@ class BackupControllerPage extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LinearPercentIndicator(
Padding(
padding: const EdgeInsets.only(top: 8.0),
lineHeight: 5.0,
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
backgroundColor: Colors.grey,
progressColor: Theme.of(context).primaryColor,
child: LinearPercentIndicator(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
barRadius: const Radius.circular(2),
lineHeight: 6.0,
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
backgroundColor: Colors.grey,
progressColor: Theme.of(context).primaryColor,
),
),
Padding(
padding: const EdgeInsets.only(top: 12.0),
@@ -92,6 +96,12 @@ class BackupControllerPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(
width: 1,
color: Color.fromARGB(255, 220, 220, 220),
),
),
onPressed: () {
isAutoBackup
? ref.watch(authenticationProvider.notifier).setAutoBackup(false)
@@ -187,6 +197,13 @@ class BackupControllerPage extends HookConsumerWidget {
),
),
trailing: OutlinedButton(
style: OutlinedButton.styleFrom(
enableFeedback: true,
side: const BorderSide(
width: 1,
color: Color.fromARGB(255, 220, 220, 220),
),
),
onPressed: () {
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
},
@@ -274,13 +291,20 @@ class BackupControllerPage extends HookConsumerWidget {
child: Container(
child: backupState.backupProgress == BackUpProgressEnum.inProgress
? ElevatedButton(
style: ElevatedButton.styleFrom(primary: Colors.red[300]),
style: ElevatedButton.styleFrom(
primary: Colors.red[300],
onPrimary: Colors.grey[50],
),
onPressed: () {
ref.read(backupProvider.notifier).cancelBackup();
},
child: const Text("Cancel"),
)
: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).primaryColor,
onPrimary: Colors.grey[50],
),
onPressed: shouldBackup
? () {
ref.read(backupProvider.notifier).startBackupProcess();
@@ -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()));
@@ -109,7 +109,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
? const Icon(Icons.backup_rounded)
: Badge(
padding: const EdgeInsets.all(4),
elevation: 1,
elevation: 2,
position: BadgePosition.bottomEnd(bottom: -4, end: -4),
badgeColor: Colors.white,
badgeContent: const Icon(
@@ -117,7 +117,6 @@ class ImmichSliverAppBar extends ConsumerWidget {
size: 8,
),
child: const Icon(Icons.backup_rounded)),
tooltip: 'Backup Controller',
onPressed: () async {
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
+124 -19
View File
@@ -1,7 +1,11 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package: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/modules/login/models/authentication_state.model.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/shared/providers/server_info.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 'dart:math';
class ProfileDrawer extends HookConsumerWidget {
const ProfileDrawer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
AuthenticationState _authState = ref.watch(authenticationProvider);
ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status;
final appInfo = useState({});
var dummmy = Random().nextInt(1024);
_getPackageInfo() async {
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(() {
_getPackageInfo();
_buildUserProfileImage();
return null;
}, []);
return Drawer(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(5),
bottomRight: Radius.circular(5),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -51,22 +114,56 @@ class ProfileDrawer extends HookConsumerWidget {
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(
color: Colors.grey[200],
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color.fromARGB(255, 216, 219, 238), Color.fromARGB(255, 226, 230, 231)],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Image(
image: AssetImage('assets/immich-logo-no-outline.png'),
width: 50,
filterQuality: FilterQuality.high,
Stack(
clipBehavior: Clip.none,
children: [
_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(
_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: const EdgeInsets.all(8.0),
child: Card(
elevation: 0,
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(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
child: Column(
@@ -8,6 +8,11 @@ class AuthenticationState {
final String userId;
final String userEmail;
final bool isAuthenticated;
final String firstName;
final String lastName;
final bool isAdmin;
final bool isFirstLogin;
final String profileImagePath;
final DeviceInfoRemote deviceInfo;
AuthenticationState({
@@ -16,6 +21,11 @@ class AuthenticationState {
required this.userId,
required this.userEmail,
required this.isAuthenticated,
required this.firstName,
required this.lastName,
required this.isAdmin,
required this.isFirstLogin,
required this.profileImagePath,
required this.deviceInfo,
});
@@ -25,6 +35,11 @@ class AuthenticationState {
String? userId,
String? userEmail,
bool? isAuthenticated,
String? firstName,
String? lastName,
bool? isAdmin,
bool? isFirstLoggedIn,
String? profileImagePath,
DeviceInfoRemote? deviceInfo,
}) {
return AuthenticationState(
@@ -33,24 +48,36 @@ class AuthenticationState {
userId: userId ?? this.userId,
userEmail: userEmail ?? this.userEmail,
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,
);
}
@override
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() {
return {
'deviceId': deviceId,
'deviceType': deviceType,
'userId': userId,
'userEmail': userEmail,
'isAuthenticated': isAuthenticated,
'deviceInfo': deviceInfo.toMap(),
};
final result = <String, dynamic>{};
result.addAll({'deviceId': deviceId});
result.addAll({'deviceType': deviceType});
result.addAll({'userId': userId});
result.addAll({'userEmail': userEmail});
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) {
@@ -60,6 +87,11 @@ class AuthenticationState {
userId: map['userId'] ?? '',
userEmail: map['userEmail'] ?? '',
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']),
);
}
@@ -78,6 +110,11 @@ class AuthenticationState {
other.userId == userId &&
other.userEmail == userEmail &&
other.isAuthenticated == isAuthenticated &&
other.firstName == firstName &&
other.lastName == lastName &&
other.isAdmin == isAdmin &&
other.isFirstLogin == isFirstLogin &&
other.profileImagePath == profileImagePath &&
other.deviceInfo == deviceInfo;
}
@@ -88,6 +125,11 @@ class AuthenticationState {
userId.hashCode ^
userEmail.hashCode ^
isAuthenticated.hashCode ^
firstName.hashCode ^
lastName.hashCode ^
isAdmin.hashCode ^
isFirstLogin.hashCode ^
profileImagePath.hashCode ^
deviceInfo.hashCode;
}
}
@@ -4,31 +4,58 @@ class LogInReponse {
final String accessToken;
final String userId;
final String userEmail;
final String firstName;
final String lastName;
final String profileImagePath;
final bool isAdmin;
final bool isFirstLogin;
LogInReponse({
required this.accessToken,
required this.userId,
required this.userEmail,
required this.firstName,
required this.lastName,
required this.profileImagePath,
required this.isAdmin,
required this.isFirstLogin,
});
LogInReponse copyWith({
String? accessToken,
String? userId,
String? userEmail,
String? firstName,
String? lastName,
String? profileImagePath,
bool? isAdmin,
bool? isFirstLogin,
}) {
return LogInReponse(
accessToken: accessToken ?? this.accessToken,
userId: userId ?? this.userId,
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() {
return {
'accessToken': accessToken,
'userId': userId,
'userEmail': userEmail,
};
final result = <String, dynamic>{};
result.addAll({'accessToken': accessToken});
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) {
@@ -36,6 +63,11 @@ class LogInReponse {
accessToken: map['accessToken'] ?? '',
userId: map['userId'] ?? '',
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));
@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
bool operator ==(Object other) {
@@ -53,9 +87,23 @@ class LogInReponse {
return other is LogInReponse &&
other.accessToken == accessToken &&
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
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;
}
}
@@ -17,9 +17,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
AuthenticationState(
deviceId: "",
deviceType: "",
isAuthenticated: false,
userId: "",
userEmail: "",
firstName: '',
lastName: '',
profileImagePath: '',
isAdmin: false,
isFirstLogin: false,
isAuthenticated: false,
deviceInfo: DeviceInfoRemote(
id: 0,
userId: "",
@@ -76,6 +81,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
isAuthenticated: true,
userId: payload.userId,
userEmail: payload.userEmail,
firstName: payload.firstName,
lastName: payload.lastName,
profileImagePath: payload.profileImagePath,
isAdmin: payload.isAdmin,
isFirstLoggedIn: payload.isFirstLogin,
);
if (isSavedLoginInfo) {
@@ -114,9 +124,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
state = AuthenticationState(
deviceId: "",
deviceType: "",
isAuthenticated: false,
userId: "",
userEmail: "",
firstName: '',
lastName: '',
profileImagePath: '',
isFirstLogin: false,
isAuthenticated: false,
isAdmin: false,
deviceInfo: DeviceInfoRemote(
id: 0,
userId: "",
@@ -139,6 +154,10 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
state = state.copyWith(deviceInfo: deviceInfoRemote);
}
updateUserProfileImagePath(String path) {
state = state.copyWith(profileImagePath: path);
}
}
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
+5 -2
View File
@@ -153,9 +153,12 @@ class LoginButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
style: ButtonStyle(
style: ElevatedButton.styleFrom(
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 {
// This will remove current cache asset state of previous user login.
@@ -79,14 +79,14 @@ class SearchResultPage extends HookConsumerWidget {
return Chip(
label: Wrap(
spacing: 5,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
currentSearchTerm.value,
style: TextStyle(color: Theme.of(context).primaryColor),
maxLines: 1,
),
Text(
currentSearchTerm.value,
style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 13, fontWeight: FontWeight.bold),
maxLines: 1,
),
Icon(
Icons.close_rounded,
@@ -13,15 +13,14 @@ class AlbumActionOutlinedButton extends StatelessWidget {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: OutlinedButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(vertical: 0, horizontal: 10)),
shape: MaterialStateProperty.resolveWith<OutlinedBorder>(
(_) => RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
side: MaterialStateProperty.resolveWith<BorderSide>(
(_) => const BorderSide(width: 1, color: Color.fromARGB(255, 158, 158, 158)),
side: const BorderSide(
width: 1,
color: Color.fromARGB(255, 215, 215, 215),
),
),
icon: Icon(iconData, size: 15),
@@ -49,7 +49,7 @@ class SharingSliverAppBar extends StatelessWidget {
),
label: const Text(
"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(
"Share with partner",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
),
),
@@ -82,11 +82,11 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
child: Padding(
padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
child: OutlinedButton.icon(
style: ButtonStyle(
alignment: Alignment.centerLeft,
padding:
MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(vertical: 22, horizontal: 16)),
),
style: OutlinedButton.styleFrom(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
side: const BorderSide(color: Color.fromARGB(255, 206, 206, 206)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))),
onPressed: _onSelectPhotosButtonPressed,
icon: const Icon(Icons.add_rounded),
label: Padding(
@@ -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;
}
@@ -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());
@@ -19,11 +19,6 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
final ServerInfoService _serverInfoService = ServerInfoService();
getMapboxInfo() async {
MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
state = state.copyWith(mapboxInfo: mapboxInfoRes);
}
getServerVersion() async {
ServerVersion? serverVersion = await _serverInfoService.getServerVersion();
@@ -1,4 +1,5 @@
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/server_version.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart';
@@ -13,15 +14,16 @@ class ServerInfoService {
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 {
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;
}
}
@@ -2,8 +2,15 @@ import 'dart:convert';
import 'package:dio/dio.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/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 {
final NetworkService _networkService = NetworkService();
@@ -21,4 +28,39 @@ class UserService {
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;
}
}
}
@@ -19,26 +19,32 @@ class TabControllerPage extends ConsumerWidget {
],
builder: (context, child, animation) {
final tabsRouter = AutoTabsRouter.of(context);
return Scaffold(
body: FadeTransition(
opacity: animation,
child: child,
return WillPopScope(
onWillPop: () async {
tabsRouter.setActiveIndex(0);
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)),
],
),
);
},
);
@@ -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;
}
}
+188 -83
View File
@@ -7,28 +7,28 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "34.0.0"
version: "38.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
version: "3.4.1"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.11"
version: "3.3.0"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.0"
version: "2.3.1"
async:
dependency: transitive
description:
@@ -42,14 +42,14 @@ packages:
name: auto_route
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.2"
version: "4.0.1"
auto_route_generator:
dependency: "direct dev"
description:
name: auto_route_generator
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.1"
version: "4.0.0"
badges:
dependency: "direct main"
description:
@@ -70,7 +70,7 @@ packages:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "2.3.0"
build_config:
dependency: transitive
description:
@@ -84,21 +84,21 @@ packages:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.1.0"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
version: "2.0.8"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.7"
version: "2.1.10"
build_runner_core:
dependency: transitive
description:
@@ -119,14 +119,14 @@ packages:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.1.4"
version: "8.3.0"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
version: "3.2.1"
cached_network_image_platform_interface:
dependency: transitive
description:
@@ -168,14 +168,7 @@ packages:
name: chewie
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.2"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
version: "1.3.2"
clock:
dependency: transitive
description:
@@ -196,7 +189,7 @@ packages:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
version: "1.16.0"
convert:
dependency: transitive
description:
@@ -204,13 +197,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.0.2"
csslib:
dependency: transitive
description:
@@ -231,14 +231,14 @@ packages:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "2.2.3"
dio:
dependency: "direct main"
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.4"
version: "4.0.6"
equatable:
dependency: "direct main"
description:
@@ -259,14 +259,14 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.0"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
version: "1.2.1"
file:
dependency: transitive
description:
@@ -280,7 +280,7 @@ packages:
name: fixnum
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.1"
flutter:
dependency: "direct main"
description: flutter
@@ -292,7 +292,7 @@ packages:
name: flutter_blurhash
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0"
version: "0.6.8"
flutter_cache_manager:
dependency: transitive
description:
@@ -306,7 +306,7 @@ packages:
name: flutter_hooks
url: "https://pub.dartlang.org"
source: hosted
version: "0.18.2"
version: "0.18.4"
flutter_launcher_icons:
dependency: "direct main"
description:
@@ -328,13 +328,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
name: flutter_riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0-dev.0"
version: "2.0.0-dev.7"
flutter_spinkit:
dependency: "direct main"
description:
@@ -372,7 +379,7 @@ packages:
name: fluttertoast
url: "https://pub.dartlang.org"
source: hosted
version: "8.0.8"
version: "8.0.9"
frontend_server_client:
dependency: transitive
description:
@@ -400,7 +407,7 @@ packages:
name: hive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "2.2.1"
hive_flutter:
dependency: "direct main"
description:
@@ -421,7 +428,7 @@ packages:
name: hooks_riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0-dev.0"
version: "2.0.0-dev.7"
html:
dependency: transitive
description:
@@ -442,7 +449,7 @@ packages:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.2.0"
http_parser:
dependency: transitive
description:
@@ -456,7 +463,42 @@ packages:
name: image
url: "https://pub.dartlang.org"
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:
dependency: "direct main"
description:
@@ -477,14 +519,14 @@ packages:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
version: "0.6.4"
json_annotation:
dependency: transitive
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "4.4.0"
version: "4.5.0"
latlong2:
dependency: transitive
description:
@@ -526,7 +568,7 @@ packages:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
version: "0.1.4"
meta:
dependency: transitive
description:
@@ -547,7 +589,7 @@ packages:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.0.2"
nested:
dependency: transitive
description:
@@ -575,14 +617,14 @@ packages:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
version: "1.4.2"
package_info_plus_linux:
dependency: transitive
description:
name: package_info_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
version: "1.0.5"
package_info_plus_macos:
dependency: transitive
description:
@@ -603,70 +645,70 @@ packages:
name: package_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "1.0.5"
package_info_plus_windows:
dependency: transitive
description:
name: package_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "1.0.5"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
version: "1.8.1"
path_provider:
dependency: transitive
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.0.10"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
version: "2.0.14"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
version: "2.0.9"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
version: "2.1.6"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "2.0.6"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
version: "2.0.4"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "2.0.6"
pedantic:
dependency: transitive
description:
@@ -680,21 +722,21 @@ packages:
name: percent_indicator
url: "https://pub.dartlang.org"
source: hosted
version: "3.4.0"
version: "4.2.2"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "4.4.0"
version: "5.0.0"
photo_manager:
dependency: "direct main"
description:
name: photo_manager
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
version: "2.1.0+2"
photo_view:
dependency: "direct main"
description:
@@ -750,14 +792,14 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
version: "6.0.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
pubspec_parse:
dependency: transitive
description:
@@ -771,14 +813,14 @@ packages:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1+1"
version: "3.1.0"
riverpod:
dependency: transitive
description:
name: riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0-dev.0"
version: "2.0.0-dev.7"
rxdart:
dependency: transitive
description:
@@ -792,7 +834,7 @@ packages:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.0"
shelf_web_socket:
dependency: transitive
description:
@@ -811,7 +853,7 @@ packages:
name: sliver_tools
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.5"
version: "0.2.6"
socket_io_client:
dependency: "direct main"
description:
@@ -832,21 +874,21 @@ packages:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
version: "1.2.2"
source_helper:
dependency: transitive
description:
name: source_helper
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
version: "1.3.2"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
version: "1.8.2"
sprintf:
dependency: transitive
description:
@@ -860,14 +902,14 @@ packages:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.0.2+1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "2.2.1+1"
stack_trace:
dependency: transitive
description:
@@ -909,7 +951,7 @@ packages:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "3.0.0+2"
term_glyph:
dependency: transitive
description:
@@ -923,7 +965,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.8"
version: "0.4.9"
timing:
dependency: transitive
description:
@@ -951,7 +993,7 @@ packages:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.3.1"
unicode:
dependency: transitive
description:
@@ -973,55 +1015,118 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.5"
version: "3.0.6"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
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:
dependency: "direct main"
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.18"
version: "2.4.2"
video_player_android:
dependency: transitive
description:
name: video_player_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.17"
version: "2.3.3"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.18"
version: "2.3.4"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.1"
version: "5.1.2"
video_player_web:
dependency: transitive
description:
name: video_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
version: "2.0.10"
visibility_detector:
dependency: "direct main"
description:
@@ -1035,7 +1140,7 @@ packages:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.6"
version: "0.6.1+2"
wakelock_macos:
dependency: transitive
description:
@@ -1077,14 +1182,14 @@ packages:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.2.0"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.8"
version: "2.5.2"
wkt_parser:
dependency: transitive
description:
@@ -1098,21 +1203,21 @@ packages:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
version: "0.2.0+1"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.3.1"
version: "5.4.1"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
version: "3.1.1"
sdks:
dart: ">=2.15.1 <3.0.0"
flutter: ">=2.8.0"
dart: ">=2.17.0 <3.0.0"
flutter: ">=3.0.0"
+9 -7
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.9.0+13
version: 1.11.0+17
environment:
sdk: ">=2.15.1 <3.0.0"
@@ -14,13 +14,13 @@ dependencies:
photo_manager: ^2.0.6
flutter_hooks: ^0.18.0
hooks_riverpod: ^2.0.0-dev.0
hive:
hive_flutter:
hive: ^2.2.1
hive_flutter: ^1.1.0
dio: ^4.0.4
cached_network_image: ^3.2.0
percent_indicator: ^3.4.0
cached_network_image: ^3.2.1
percent_indicator: ^4.2.2
intl: ^0.17.0
auto_route: ^3.2.2
auto_route: ^4.0.1
exif: ^3.1.1
transparent_image: ^2.0.0
visibility_detector: ^0.2.2
@@ -38,6 +38,8 @@ dependencies:
flutter_spinkit: ^5.1.0
flutter_swipe_detector: ^2.0.0
equatable: ^2.0.3
image_picker: ^0.8.5+3
url_launcher: ^6.1.3
dev_dependencies:
flutter_test:
@@ -45,7 +47,7 @@ dev_dependencies:
flutter_lints: ^1.0.0
hive_generator: ^1.1.2
build_runner: ^2.1.7
auto_route_generator: ^3.2.1
auto_route_generator: ^4.0.0
flutter:
uses-material-design: true
+2 -1
View File
@@ -1,5 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120
"printWidth": 120,
"semi": true
}
+4 -4
View File
@@ -1,4 +1,4 @@
FROM node:16-alpine3.14
FROM node:16-alpine3.14 as core
ARG DEBIAN_FRONTEND=noninteractive
@@ -6,10 +6,10 @@ WORKDIR /usr/src/app
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 . .
RUN npm run build
RUN npm run build
@@ -19,11 +19,11 @@ import {
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AssetService } from './asset.service';
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 { CreateAssetDto } from './dto/create-asset.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 { Response as Res } from 'express';
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 { SearchAssetDto } from './dto/search-asset.dto';
import { CommunicationGateway } from '../communication/communication.gateway';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
@UseGuards(JwtAuthGuard)
@Controller('asset')
@@ -39,6 +41,9 @@ export class AssetController {
private wsCommunicateionGateway: CommunicationGateway,
private assetService: AssetService,
private backgroundTaskService: BackgroundTaskService,
@InjectQueue('asset-uploaded-queue')
private assetUploadedQueue: Queue,
) {}
@Post('upload')
@@ -48,7 +53,7 @@ export class AssetController {
{ name: 'assetData', maxCount: 1 },
{ name: 'thumbnailData', maxCount: 1 },
],
multerOption,
assetUploadOption,
),
)
async uploadFile(
@@ -61,12 +66,23 @@ export class AssetController {
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
if (uploadFiles.thumbnailData != null && savedAsset) {
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
await this.backgroundTaskService.detectObject(uploadFiles.thumbnailData[0].path, savedAsset);
}
const assetWithThumbnail = await this.assetService.updateThumbnailInfo(
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));
} catch (e) {
@@ -123,7 +139,7 @@ export class AssetController {
@Get('/')
async getAllAssets(@GetAuthUser() authUser: AuthUserDto) {
return await this.assetService.getAllAssetsNoPagination(authUser);
return await this.assetService.getAllAssets(authUser);
}
@Get('/:deviceId')
@@ -2,9 +2,7 @@ import { Module } from '@nestjs/common';
import { AssetService } from './asset.service';
import { AssetController } from './asset.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from './entities/asset.entity';
import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module';
import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
import { AssetEntity } from '@app/database/entities/asset.entity';
import { BullModule } from '@nestjs/bull';
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
@@ -13,29 +11,19 @@ import { CommunicationModule } from '../communication/communication.module';
@Module({
imports: [
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,
TypeOrmModule.forFeature([AssetEntity]),
BullModule.registerQueue({
name: 'asset-uploaded-queue',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
],
controllers: [AssetController],
providers: [AssetService, AssetOptimizeService, BackgroundTaskService],
providers: [AssetService, BackgroundTaskService],
exports: [],
})
export class AssetModule {}
@@ -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 { MoreThan, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
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 { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
import { createReadStream, stat } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto';
import { Response as Res } from 'express';
import { promisify } from 'util';
import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto';
import path from 'path';
const fileInfo = promisify(stat);
@@ -24,10 +21,16 @@ export class AssetService {
private assetRepository: Repository<AssetEntity>,
) {}
public async updateThumbnailInfo(assetId: string, path: string) {
return await this.assetRepository.update(assetId, {
resizePath: path,
});
public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise<AssetEntity> {
const updatedAsset = await this.assetRepository
.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) {
@@ -64,13 +67,17 @@ export class AssetService {
return res;
}
public async getAllAssetsNoPagination(authUser: AuthUserDto) {
public async getAllAssets(authUser: AuthUserDto) {
try {
return await this.assetRepository
.createQueryBuilder('a')
.where('a."userId" = :userId', { userId: authUser.id })
.orderBy('a."createdAt"::date', 'DESC')
.getMany();
return await this.assetRepository.find({
where: {
userId: authUser.id,
},
relations: ['exifInfo'],
order: {
createdAt: 'DESC',
},
});
} catch (e) {
Logger.error(e, 'getAllAssets');
}
@@ -99,96 +106,164 @@ export class AssetService {
}
public async downloadFile(query: ServeFileDto, res: Res) {
let file = null;
const asset = await this.findOne(query.did, query.aid);
try {
let file = null;
const asset = await this.findOne(query.did, query.aid);
if (query.isThumb === 'false' || !query.isThumb) {
file = createReadStream(asset.originalPath);
} else {
file = createReadStream(asset.resizePath);
if (query.isThumb === 'false' || !query.isThumb) {
const { size } = await fileInfo(asset.originalPath);
res.set({
'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) {
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) {
let file = null;
const asset = await this.findOne(query.did, query.aid);
if (!asset) {
throw new BadRequestException('Asset does not exist');
}
// Handle Sending Images
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
res.set({
'Content-Type': asset.mimeType,
});
if (query.isThumb === 'false' || !query.isThumb) {
file = createReadStream(asset.originalPath);
} else {
file = createReadStream(asset.resizePath);
/**
* Serve file viewer on the web
*/
if (query.isWeb) {
res.set({
'Content-Type': 'image/jpeg',
});
return new StreamableFile(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);
try {
/**
* Serve thumbnail image for both web and mobile app
*/
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) {
// Handle Handling Video
const { size } = await fileInfo(asset.originalPath);
const range = headers.range;
try {
// Handle Video
let videoPath = asset.originalPath;
let mimeType = asset.mimeType;
if (range) {
/** Extracting Start and End value from Range Header */
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;
if (query.isWeb && asset.mimeType == 'video/quicktime') {
videoPath = asset.encodedVideoPath == '' ? asset.originalPath : asset.encodedVideoPath;
mimeType = asset.encodedVideoPath == '' ? asset.mimeType : 'video/mp4';
}
// 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}`,
const { size } = await fileInfo(videoPath);
const range = headers.range;
if (range) {
/** Extracting Start and End value from Range Header */
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));
}
/** 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': 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));
} catch (e) {
Logger.error('Error serving VIDEO asset ', e);
throw new InternalServerErrorException(`Failed to serve video asset ${e}`, 'ServeFile');
}
}
}
@@ -1,5 +1,5 @@
import { IsNotEmpty, IsOptional } from 'class-validator';
import { AssetType } from '../entities/asset.entity';
import { AssetType } from '@app/database/entities/asset.entity';
export class CreateAssetDto {
@IsNotEmpty()

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