Compare commits
32 Commits
v1.9.0_13-
...
v1.10.0_15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b34de624ce | ||
|
|
7886c42742 | ||
|
|
d476b15312 | ||
|
|
bdf38e7668 | ||
|
|
e33566a04a | ||
|
|
c28251b8b4 | ||
|
|
337db1c508 | ||
|
|
ad2a1ba901 | ||
|
|
fa6f6f8e9f | ||
|
|
a44043a4e5 | ||
|
|
87b15c60c0 | ||
|
|
2c83e52c15 | ||
|
|
55c5027539 | ||
|
|
ce06af0c9b | ||
|
|
baaf7ad153 | ||
|
|
4a25e7dc22 | ||
|
|
f90563d18c | ||
|
|
8352ecd3b9 | ||
|
|
69b34a4364 | ||
|
|
6023c3c624 | ||
|
|
171e7ffa77 | ||
|
|
d9f918005a | ||
|
|
e8ade4866b | ||
|
|
bbfa789a4e | ||
|
|
a779c3803c | ||
|
|
79dea504b0 | ||
|
|
4900fecd10 | ||
|
|
adfaab7eb2 | ||
|
|
c5adbea6e1 | ||
|
|
bb89fa4aab | ||
|
|
43d639104d | ||
|
|
a1792a7d94 |
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,15 +1,26 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: "[BUG]"
|
title: '[BUG] <title>'
|
||||||
labels: bug
|
labels: bug, need triage
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Note: Please search to see if an issue already exists for the bug you encountered.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Task List**
|
||||||
|
[ ] I have read thoroughly the README setup and installation instructions.
|
||||||
|
[ ] If my setup is different, I have included my docker-compose file.
|
||||||
|
[ ] I have included my redacted `.env` file.
|
||||||
|
[ ] I have included information on my machine, and environment.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
32
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
32
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Request a feature that you would like for the app
|
||||||
|
title: "[Feature]: "
|
||||||
|
labels: ["feature", "need triage"]
|
||||||
|
assignees:
|
||||||
|
- ""
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this feature request!
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-detail
|
||||||
|
attributes:
|
||||||
|
label: Feature detail
|
||||||
|
placeholder: Describe the feature you would like to see for the app
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
description: Choose the platform for the feature
|
||||||
|
options:
|
||||||
|
- Web
|
||||||
|
- Mobile App
|
||||||
|
- Server
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
86
.github/workflows/build_push_docker_latest.yml
vendored
86
.github/workflows/build_push_docker_latest.yml
vendored
@@ -8,14 +8,15 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build_and_push_server_latest:
|
build_and_push_server_latest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: "main" # branch
|
# ref: "main" # branch
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.0.0
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -37,28 +38,59 @@ jobs:
|
|||||||
altran1502/immich-server:latest
|
altran1502/immich-server:latest
|
||||||
|
|
||||||
build_and_push_microservice_latest:
|
build_and_push_microservice_latest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: "main" # branch
|
# ref: "main" # branch
|
||||||
- name: Set up QEMU
|
fetch-depth: 0
|
||||||
uses: docker/setup-qemu-action@v2.0.0
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up QEMU
|
||||||
id: buildx
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
uses: docker/setup-buildx-action@v2.0.0
|
- name: Set up Docker Buildx
|
||||||
- name: Login to Docker Hub
|
id: buildx
|
||||||
uses: docker/login-action@v2
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
with:
|
- name: Login to Docker Hub
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
uses: docker/login-action@v2
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
with:
|
||||||
- name: Build and Push Microservices
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
uses: docker/build-push-action@v3.0.0
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
with:
|
- name: Build and Push Microservices
|
||||||
context: ./microservices
|
uses: docker/build-push-action@v3.0.0
|
||||||
file: ./microservices/Dockerfile
|
with:
|
||||||
platforms: linux/arm/v7,linux/amd64
|
context: ./microservices
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
file: ./microservices/Dockerfile
|
||||||
tags: |
|
platforms: linux/arm/v7,linux/amd64
|
||||||
altran1502/immich-microservices:latest
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-microservices:latest
|
||||||
|
|
||||||
|
build_and_push_web_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
|
||||||
|
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:latest
|
||||||
|
|||||||
49
.github/workflows/build_push_server_release.yml
vendored
49
.github/workflows/build_push_server_release.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
|||||||
ref: "main"
|
ref: "main"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: 'Get Previous tag'
|
- name: "Get Previous tag"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
with:
|
with:
|
||||||
fallback: latest
|
fallback: latest
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.0.0
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
@@ -53,11 +53,11 @@ jobs:
|
|||||||
ref: "main"
|
ref: "main"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: 'Get Previous tag'
|
- name: "Get Previous tag"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
with:
|
with:
|
||||||
fallback: latest
|
fallback: latest
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.0.0
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
@@ -80,4 +80,43 @@ jobs:
|
|||||||
platforms: linux/arm/v7,linux/amd64
|
platforms: linux/arm/v7,linux/amd64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-microservices:${{ steps.previoustag.outputs.tag }}
|
altran1502/immich-microservices:${{ steps.previoustag.outputs.tag }}
|
||||||
|
|
||||||
|
build_and_push_web_release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: "main"
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: "Get Previous tag"
|
||||||
|
id: previoustag
|
||||||
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
|
with:
|
||||||
|
fallback: latest
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push immich-web release
|
||||||
|
uses: docker/build-push-action@v3.0.0
|
||||||
|
with:
|
||||||
|
context: ./web
|
||||||
|
file: ./web/Dockerfile
|
||||||
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
target: prod
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-web:${{ steps.previoustag.outputs.tag }}
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -7,6 +7,12 @@ dev-update:
|
|||||||
dev-scale:
|
dev-scale:
|
||||||
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans
|
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans
|
||||||
|
|
||||||
|
stage:
|
||||||
|
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
test-e2e:
|
||||||
|
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich_server_test
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
|||||||
9
NOTES.md
Normal file
9
NOTES.md
Normal 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
|
||||||
94
README.md
94
README.md
@@ -31,6 +31,7 @@ Loading ~4000 images/videos
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
### Mobile client
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/login-screen.png" width="150" title="Login With Custom URL">
|
<img src="design/login-screen.png" width="150" title="Login With Custom URL">
|
||||||
<img src="design/backup-screen.png" width="150" title="Backup Setting Info">
|
<img src="design/backup-screen.png" width="150" title="Backup Setting Info">
|
||||||
@@ -39,14 +40,18 @@ Loading ~4000 images/videos
|
|||||||
<img src="design/search-screen.jpeg" width="150" title="Curated Search Info">
|
<img src="design/search-screen.jpeg" width="150" title="Curated Search Info">
|
||||||
<img src="design/shared-albums.png" width="150" title="Shared Albums">
|
<img src="design/shared-albums.png" width="150" title="Shared Albums">
|
||||||
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Web client
|
||||||
|
<p align="center">
|
||||||
|
<img src="design/dashboard_photos.jpeg" width="100%" title="Home Dashboard">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Note
|
# Note
|
||||||
|
|
||||||
**!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
|
**!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
|
||||||
|
|
||||||
This project is under heavy development, there will be continous functions, features and api changes.
|
This project is under heavy development, there will be continuous functions, features and api changes.
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
@@ -67,29 +72,30 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
- Show curated objects on the search page
|
- Show curated objects on the search page
|
||||||
- Shared album with users on the same server
|
- Shared album with users on the same server
|
||||||
- Selective backup - albums can be included and excluded during the backup process.
|
- Selective backup - albums can be included and excluded during the backup process.
|
||||||
|
- Web interface is available for administrative tasks (creating new users) and viewing assets on the server - additional features are coming.
|
||||||
|
|
||||||
# System Requirement
|
# System Requirement
|
||||||
|
|
||||||
**OS**: Preferred Linux-based operating system (Ubuntu, Debian, MacOS...etc).
|
**OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
|
||||||
|
|
||||||
I haven't tested with `Docker for Windows` as well as `WSL` on Windows
|
I haven't tested with `Docker for Windows` as well as `WSL` on Windows
|
||||||
|
|
||||||
*Raspberry Pi can be used but `microservices` container has to be comment out in `docker-compose` since TensorFlow has not been supported in Dockec image on arm64v7 yet.*
|
*Raspberry Pi can be used but `microservices` container has to be comment out in `docker-compose` since TensorFlow has not been supported in Docker image on arm64v7 yet.*
|
||||||
|
|
||||||
**RAM**: At least 2GB, preffered 4GB.
|
**RAM**: At least 2GB, preffered 4GB.
|
||||||
|
|
||||||
**Core**: At least 2 cores, preffered 4 cores.
|
**Core**: At least 2 cores, preffered 4 cores.
|
||||||
|
|
||||||
# Development and Testing out the application
|
# Getting Started
|
||||||
|
|
||||||
You can use docker compose for development and testing out the application, there are several services that compose Immich:
|
You can use docker compose for development and testing out the application, there are several services that compose Immich:
|
||||||
|
|
||||||
1. **NestJs** - Backend of the application
|
1. **NestJs** - Backend of the application
|
||||||
2. **PostgreSQL** - Main database of the application
|
2. **SvelteKit** - Web frontend of the application
|
||||||
3. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
|
3. **PostgreSQL** - Main database of the application
|
||||||
4. **Nginx** - Load balancing and optimized file uploading.
|
4. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
|
||||||
5. **TensorFlow** - Object Detection and Image Classification.
|
5. **Nginx** - Load balancing and optimized file uploading.
|
||||||
|
6. **TensorFlow** - Object Detection and Image Classification.
|
||||||
|
|
||||||
## Step 1: Populate .env file
|
## Step 1: Populate .env file
|
||||||
|
|
||||||
@@ -108,52 +114,75 @@ Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is own
|
|||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
###################################################################################
|
||||||
# Database
|
# Database
|
||||||
|
###################################################################################
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
DB_DATABASE_NAME=immich
|
DB_DATABASE_NAME=immich
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
# Upload File Config
|
# Upload File Config
|
||||||
|
###################################################################################
|
||||||
UPLOAD_LOCATION=<put-the-path-of-the-upload-folder-here>
|
UPLOAD_LOCATION=<put-the-path-of-the-upload-folder-here>
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
# JWT SECRET
|
# JWT SECRET
|
||||||
|
###################################################################################
|
||||||
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
# MAPBOX
|
# MAPBOX
|
||||||
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
####################################################################################
|
||||||
|
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
ENABLE_MAPBOX=false
|
ENABLE_MAPBOX=false
|
||||||
MAPBOX_KEY=
|
MAPBOX_KEY=
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# WEB
|
||||||
|
###################################################################################
|
||||||
|
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
||||||
|
# know where can it make the request to.
|
||||||
|
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
||||||
|
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283
|
||||||
|
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 2: Start the server
|
## Step 2: Start the server
|
||||||
|
|
||||||
To start, run
|
To **start**, run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f ./docker/docker-compose.yml up
|
docker-compose -f ./docker/docker-compose.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
If you have a few thousand photos/videos, I suggest running docker-compose with scaling option for the `immich_server` container to handle high I/O load when using fast scrolling.
|
If you have a few thousand photos/videos, I suggest running docker-compose with *scaling* option for the `immich_server` container to handle high I/O load when using fast scrolling.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f ./docker/docker-compose.yml up --scale immich_server=5
|
docker-compose -f ./docker/docker-compose.yml up --scale immich-server=5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To *update* docker-compose with newest image (if you have started the docker-compose previously)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
||||||
|
```
|
||||||
|
|
||||||
The server will be running at `http://your-ip:2283` through `Nginx`
|
The server will be running at `http://your-ip:2283` through `Nginx`
|
||||||
|
|
||||||
## Step 3: Register User
|
## Step 3: Register User
|
||||||
|
|
||||||
Use the command below on your terminal to create user as we don't have user interface for this function yet.
|
Access the web interface at `http://your-ip:2285` to register an admin account.
|
||||||
|
|
||||||
```bash
|
<p align="left">
|
||||||
curl --location --request POST 'http://your-server-ip:2283/auth/signUp' \
|
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
||||||
--header 'Content-Type: application/json' \
|
<p/>
|
||||||
--data-raw '{
|
|
||||||
"email": "testuser@email.com",
|
Additional accounts on the server can be created by the admin account.
|
||||||
"password": "password"
|
|
||||||
}'
|
<p align="left">
|
||||||
```
|
<img src="design/admin-interface.png" width="500" title="Admin User Management">
|
||||||
|
<p/>
|
||||||
|
|
||||||
## Step 4: Run mobile app
|
## Step 4: Run mobile app
|
||||||
|
|
||||||
@@ -188,15 +217,26 @@ You can get the app on F-droid by clicking the image below.
|
|||||||
<img src="design/ios-qr-code.png" width="200" title="Apple App Store">
|
<img src="design/ios-qr-code.png" width="200" title="Apple App Store">
|
||||||
<p/>
|
<p/>
|
||||||
|
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
The development environment can be started from the root of the project after populating the `.env` file with the command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev # required Makefile installed on the system.
|
||||||
|
```
|
||||||
|
|
||||||
|
All servers and web container are hot reload for quick feedback loop.
|
||||||
|
|
||||||
# Support
|
# Support
|
||||||
|
|
||||||
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsore**](https://github.com/sponsors/alextran1502), or one time donation with Buy Me a coffee link below.
|
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below.
|
||||||
|
|
||||||
[](https://www.buymeacoffee.com/altran1502)
|
[](https://www.buymeacoffee.com/altran1502)
|
||||||
|
|
||||||
This is also a meaningful way to give me motivation and encounragment to continue working on the app.
|
This is also a meaningful way to give me motivation and encouragement to continue working on the app.
|
||||||
|
|
||||||
Cheer! 🎉
|
Cheers! 🎉
|
||||||
|
|
||||||
# Known Issue
|
# Known Issue
|
||||||
|
|
||||||
@@ -204,13 +244,13 @@ Cheer! 🎉
|
|||||||
|
|
||||||
*This is a known issue on RaspberryPi 4 arm64-v7 and incorrect Promox setup*
|
*This is a known issue on RaspberryPi 4 arm64-v7 and incorrect Promox setup*
|
||||||
|
|
||||||
TensorFlow doesn't run with older CPU architecture, it requires CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`:
|
TensorFlow doesn't run with older CPU architecture, it requires a CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
more /proc/cpuinfo | grep flags
|
more /proc/cpuinfo | grep flags
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are running virtualization in Promox, the VM doesn't have the flag enable.
|
If you are running virtualization in Promox, the VM doesn't have the flag enabled.
|
||||||
|
|
||||||
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
|
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
|
||||||
|
|
||||||
|
|||||||
BIN
design/admin-interface.png
Normal file
BIN
design/admin-interface.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
design/admin-registration-form.png
Normal file
BIN
design/admin-registration-form.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
design/dashboard_photos.jpeg
Normal file
BIN
design/dashboard_photos.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
@@ -1,15 +1,63 @@
|
|||||||
|
###################################################################################
|
||||||
# Database
|
# Database
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
DB_HOSTNAME=immich_postgres
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
DB_DATABASE_NAME=immich
|
DB_DATABASE_NAME=immich
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# Redis
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
REDIS_HOSTNAME=immich_redis
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
# Upload File Config
|
# Upload File Config
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
# JWT SECRET
|
# JWT SECRET
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
# MAPBOX
|
# MAPBOX
|
||||||
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
####################################################################################
|
||||||
|
|
||||||
|
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
ENABLE_MAPBOX=false
|
ENABLE_MAPBOX=false
|
||||||
MAPBOX_KEY=
|
MAPBOX_KEY=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# WEB
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
||||||
|
# know where can it make the request to.
|
||||||
|
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
||||||
|
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283
|
||||||
|
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
|
||||||
|
|
||||||
|
VITE_SERVER_ENDPOINT=
|
||||||
|
|||||||
16
docker/.env.test
Normal file
16
docker/.env.test
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
DB_HOSTNAME=immich_postgres_test
|
||||||
|
# Database
|
||||||
|
DB_USERNAME=postgres
|
||||||
|
DB_PASSWORD=postgres
|
||||||
|
DB_DATABASE_NAME=e2e_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
|
||||||
|
MAPBOX_KEY=
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich-server:
|
||||||
image: immich-server-dev:1.9.0
|
image: immich-server-dev:1.9.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
@@ -21,9 +21,9 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
immich_microservices:
|
immich-microservices:
|
||||||
image: immich-microservices-dev:1.9.0
|
image: immich-microservices-dev:1.9.0
|
||||||
build:
|
build:
|
||||||
context: ../microservices
|
context: ../microservices
|
||||||
@@ -42,14 +42,32 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
|
immich-web:
|
||||||
|
image: immich-web-dev:1.9.0
|
||||||
|
build:
|
||||||
|
context: ../web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: dev
|
||||||
|
command: npm run dev --host
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 3002:3002
|
||||||
|
- 24678:24678
|
||||||
|
volumes:
|
||||||
|
- ../web:/usr/src/app
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -66,7 +84,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
container_name: proxy_nginx
|
container_name: proxy_nginx
|
||||||
@@ -79,11 +97,11 @@ services:
|
|||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich-server
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich-network:
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich-server:
|
||||||
image: immich-server-dev:1.9.0
|
image: immich-server-dev:1.9.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
@@ -19,9 +19,9 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
immich_microservices:
|
immich-microservices:
|
||||||
image: immich-microservices-dev:1.9.0
|
image: immich-microservices-dev:1.9.0
|
||||||
build:
|
build:
|
||||||
context: ../microservices
|
context: ../microservices
|
||||||
@@ -46,13 +46,13 @@ services:
|
|||||||
- database
|
- database
|
||||||
- immich_server
|
- immich_server
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -69,7 +69,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
container_name: proxy_nginx
|
container_name: proxy_nginx
|
||||||
@@ -82,11 +82,11 @@ services:
|
|||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich-server
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich-network:
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
105
docker/docker-compose.staging.yml
Normal file
105
docker/docker-compose.staging.yml
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
image: immich-server-staging:latest
|
||||||
|
build:
|
||||||
|
context: ../server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
entrypoint: ["/bin/sh", "./entrypoint.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: immich-microservices-staging:latest
|
||||||
|
build:
|
||||||
|
context: ../microservices
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
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: immich-web-staging:latest
|
||||||
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
|
build:
|
||||||
|
context: ../web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: prod
|
||||||
|
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
|
||||||
|
|
||||||
|
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:
|
||||||
52
docker/docker-compose.test.yml
Normal file
52
docker/docker-compose.test.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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:
|
||||||
|
- pgdata-test:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
networks:
|
||||||
|
- immich_network_test
|
||||||
|
|
||||||
|
networks:
|
||||||
|
immich_network_test:
|
||||||
|
volumes:
|
||||||
|
pgdata-test:
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich-server:
|
||||||
image: altran1502/immich-server:v1.8.0_12-dev
|
image: altran1502/immich-server:latest
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
@@ -16,11 +16,11 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
|
|
||||||
immich_microservices:
|
immich-microservices:
|
||||||
image: altran1502/immich-microservices:v1.8.0_12-dev
|
image: altran1502/immich-microservices:latest
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
expose:
|
||||||
- "3001"
|
- "3001"
|
||||||
@@ -33,14 +33,28 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
|
|
||||||
|
immich-web:
|
||||||
|
image: altran1502/immich-web:latest
|
||||||
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 2285:3000
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -57,7 +71,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
container_name: proxy_nginx
|
container_name: proxy_nginx
|
||||||
@@ -70,11 +85,12 @@ services:
|
|||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich-server
|
||||||
|
restart: always
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich-network:
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
@@ -41,6 +41,6 @@ server {
|
|||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
proxy_pass http://immich_server:3000;
|
proxy_pass http://immich-server:3000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
export const databaseConfig: TypeOrmModuleOptions = {
|
export const databaseConfig: TypeOrmModuleOptions = {
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: 'immich_postgres',
|
host: process.env.DB_HOSTNAME || 'immich_postgres',
|
||||||
port: 5432,
|
port: 5432,
|
||||||
username: process.env.DB_USERNAME,
|
username: process.env.DB_USERNAME,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ImageClassifierService } from './image-classifier.service';
|
|||||||
export class ImageClassifierController {
|
export class ImageClassifierController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly imageClassifierService: ImageClassifierService,
|
private readonly imageClassifierService: ImageClassifierService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post('/tagImage')
|
@Post('/tagImage')
|
||||||
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
|
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Body, Controller, Post } from '@nestjs/common';
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
import { ObjectDetectionService } from './object-detection.service';
|
import { ObjectDetectionService } from './object-detection.service';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
@Controller('object-detection')
|
@Controller('object-detection')
|
||||||
export class ObjectDetectionController {
|
export class ObjectDetectionController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly objectDetectionService: ObjectDetectionService,
|
private readonly objectDetectionService: ObjectDetectionService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post('/detectObject')
|
@Post('/detectObject')
|
||||||
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
|
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
|
||||||
<application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher">
|
<application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
|
||||||
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
// Generated file.
|
// Generated file.
|
||||||
|
//
|
||||||
// If you wish to remove Flutter's multidex support, delete this entire file.
|
// If you wish to remove Flutter's multidex support, delete this entire file.
|
||||||
|
//
|
||||||
|
// Modifications to this file should be done in a copy under a different name
|
||||||
|
// as this file may be regenerated.
|
||||||
|
|
||||||
package io.flutter.app;
|
package io.flutter.app;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.multidex.MultiDex;
|
import androidx.multidex.MultiDex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
|
* Extension of {@link android.app.Application}, adding multidex support.
|
||||||
*/
|
*/
|
||||||
public class FlutterMultiDexApplication extends FlutterApplication {
|
public class FlutterMultiDexApplication extends Application {
|
||||||
@Override
|
@Override
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected void attachBaseContext(Context base) {
|
protected void attachBaseContext(Context base) {
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -9,6 +9,8 @@ PODS:
|
|||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 2.7.5)
|
||||||
- FMDB/standard (2.7.5)
|
- FMDB/standard (2.7.5)
|
||||||
|
- image_picker_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
@@ -30,6 +32,7 @@ DEPENDENCIES:
|
|||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
@@ -50,6 +53,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
|
image_picker_ios:
|
||||||
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
@@ -66,10 +71,11 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||||
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
||||||
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
|
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
|
|||||||
@@ -360,7 +360,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -522,7 +522,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>1.10.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2</string>
|
<string>14</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true />
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
@@ -43,6 +43,12 @@
|
|||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
|
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||||
|
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||||
|
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -68,5 +74,7 @@
|
|||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false />
|
<false />
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true />
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.9.0"
|
version_number: "1.10.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
|
|||||||
[bundle exec] fastlane ios beta
|
[bundle exec] fastlane ios beta
|
||||||
```
|
```
|
||||||
|
|
||||||
iOS deployment
|
iOS Beta
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|||||||
@@ -5,27 +5,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000332">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000946">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: latest_testflight_build_number" time="4.608292">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="16.3225">
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: increment_build_number" time="0.747162">
|
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: build_app" time="88.727281">
|
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: upload_to_testflight" time="7.79397">
|
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initApp() async {
|
Future<void> initApp() async {
|
||||||
WidgetsBinding.instance?.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -87,7 +87,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance?.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,5 +38,7 @@ class HiveBackupAlbumsAdapter extends TypeAdapter<HiveBackupAlbums> {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is HiveBackupAlbumsAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
other is HiveBackupAlbumsAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class BackupService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Build thumbnail multipart data
|
// Build thumbnail multipart data
|
||||||
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280));
|
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
|
||||||
if (thumbnailData != null) {
|
if (thumbnailData != null) {
|
||||||
thumbnailUploadData = MultipartFile.fromBytes(
|
thumbnailUploadData = MultipartFile.fromBytes(
|
||||||
List.from(thumbnailData),
|
List.from(thumbnailData),
|
||||||
|
|||||||
@@ -45,12 +45,16 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
LinearPercentIndicator(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
lineHeight: 5.0,
|
child: LinearPercentIndicator(
|
||||||
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
backgroundColor: Colors.grey,
|
barRadius: const Radius.circular(2),
|
||||||
progressColor: Theme.of(context).primaryColor,
|
lineHeight: 6.0,
|
||||||
|
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
||||||
|
backgroundColor: Colors.grey,
|
||||||
|
progressColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 12.0),
|
padding: const EdgeInsets.only(top: 12.0),
|
||||||
|
|||||||
@@ -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()));
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
@@ -9,17 +13,21 @@ import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
|||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
class ProfileDrawer extends HookConsumerWidget {
|
class ProfileDrawer extends HookConsumerWidget {
|
||||||
const ProfileDrawer({Key? key}) : super(key: key);
|
const ProfileDrawer({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
AuthenticationState _authState = ref.watch(authenticationProvider);
|
AuthenticationState _authState = ref.watch(authenticationProvider);
|
||||||
ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status;
|
||||||
final appInfo = useState({});
|
final appInfo = useState({});
|
||||||
|
var dummmy = Random().nextInt(1024);
|
||||||
|
|
||||||
_getPackageInfo() async {
|
_getPackageInfo() async {
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
@@ -30,19 +38,74 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildUserProfileImage() {
|
||||||
|
if (_authState.profileImagePath.isEmpty) {
|
||||||
|
return const CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
||||||
|
if (_authState.profileImagePath.isNotEmpty) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
|
||||||
|
return const CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
|
||||||
|
return const ImmichLoadingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
_pickUserProfileImage() async {
|
||||||
|
final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
|
||||||
|
|
||||||
|
if (image != null) {
|
||||||
|
var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
ref
|
||||||
|
.watch(authenticationProvider.notifier)
|
||||||
|
.updateUserProfileImagePath(ref.read(uploadProfileImageProvider).profileImagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
_getPackageInfo();
|
_getPackageInfo();
|
||||||
|
_buildUserProfileImage();
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topRight: Radius.circular(5),
|
|
||||||
bottomRight: Radius.circular(5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -51,22 +114,60 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
children: [
|
children: [
|
||||||
DrawerHeader(
|
DrawerHeader(
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.grey[200],
|
gradient: LinearGradient(
|
||||||
|
colors: [Color.fromARGB(255, 216, 219, 238), Color.fromARGB(255, 226, 230, 231)],
|
||||||
|
begin: Alignment.centerRight,
|
||||||
|
end: Alignment.centerLeft,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Image(
|
Stack(
|
||||||
image: AssetImage('assets/immich-logo-no-outline.png'),
|
clipBehavior: Clip.none,
|
||||||
width: 50,
|
children: [
|
||||||
filterQuality: FilterQuality.high,
|
_buildUserProfileImage(),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: -5,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _pickUserProfileImage,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(5.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.edit,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const Padding(padding: EdgeInsets.all(8)),
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
Text(
|
Text(
|
||||||
_authState.userEmail,
|
"${_authState.firstName} ${_authState.lastName}",
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4.0),
|
||||||
|
child: Text(
|
||||||
|
_authState.userEmail,
|
||||||
|
style: TextStyle(color: Colors.grey[800], fontSize: 12),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -97,7 +198,15 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Card(
|
child: Card(
|
||||||
|
elevation: 0,
|
||||||
color: Colors.grey[100],
|
color: Colors.grey[100],
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Color.fromARGB(101, 201, 201, 201),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ class AuthenticationState {
|
|||||||
final String userId;
|
final String userId;
|
||||||
final String userEmail;
|
final String userEmail;
|
||||||
final bool isAuthenticated;
|
final bool isAuthenticated;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
|
final bool isAdmin;
|
||||||
|
final bool isFirstLogin;
|
||||||
|
final String profileImagePath;
|
||||||
final DeviceInfoRemote deviceInfo;
|
final DeviceInfoRemote deviceInfo;
|
||||||
|
|
||||||
AuthenticationState({
|
AuthenticationState({
|
||||||
@@ -16,6 +21,11 @@ class AuthenticationState {
|
|||||||
required this.userId,
|
required this.userId,
|
||||||
required this.userEmail,
|
required this.userEmail,
|
||||||
required this.isAuthenticated,
|
required this.isAuthenticated,
|
||||||
|
required this.firstName,
|
||||||
|
required this.lastName,
|
||||||
|
required this.isAdmin,
|
||||||
|
required this.isFirstLogin,
|
||||||
|
required this.profileImagePath,
|
||||||
required this.deviceInfo,
|
required this.deviceInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,6 +35,11 @@ class AuthenticationState {
|
|||||||
String? userId,
|
String? userId,
|
||||||
String? userEmail,
|
String? userEmail,
|
||||||
bool? isAuthenticated,
|
bool? isAuthenticated,
|
||||||
|
String? firstName,
|
||||||
|
String? lastName,
|
||||||
|
bool? isAdmin,
|
||||||
|
bool? isFirstLoggedIn,
|
||||||
|
String? profileImagePath,
|
||||||
DeviceInfoRemote? deviceInfo,
|
DeviceInfoRemote? deviceInfo,
|
||||||
}) {
|
}) {
|
||||||
return AuthenticationState(
|
return AuthenticationState(
|
||||||
@@ -33,24 +48,36 @@ class AuthenticationState {
|
|||||||
userId: userId ?? this.userId,
|
userId: userId ?? this.userId,
|
||||||
userEmail: userEmail ?? this.userEmail,
|
userEmail: userEmail ?? this.userEmail,
|
||||||
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
||||||
|
firstName: firstName ?? this.firstName,
|
||||||
|
lastName: lastName ?? this.lastName,
|
||||||
|
isAdmin: isAdmin ?? this.isAdmin,
|
||||||
|
isFirstLogin: isFirstLoggedIn ?? isFirstLogin,
|
||||||
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
deviceInfo: deviceInfo ?? this.deviceInfo,
|
deviceInfo: deviceInfo ?? this.deviceInfo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, deviceInfo: $deviceInfo)';
|
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, isFirstLoggedIn: $isFirstLogin, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)';
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
final result = <String, dynamic>{};
|
||||||
'deviceId': deviceId,
|
|
||||||
'deviceType': deviceType,
|
result.addAll({'deviceId': deviceId});
|
||||||
'userId': userId,
|
result.addAll({'deviceType': deviceType});
|
||||||
'userEmail': userEmail,
|
result.addAll({'userId': userId});
|
||||||
'isAuthenticated': isAuthenticated,
|
result.addAll({'userEmail': userEmail});
|
||||||
'deviceInfo': deviceInfo.toMap(),
|
result.addAll({'isAuthenticated': isAuthenticated});
|
||||||
};
|
result.addAll({'firstName': firstName});
|
||||||
|
result.addAll({'lastName': lastName});
|
||||||
|
result.addAll({'isAdmin': isAdmin});
|
||||||
|
result.addAll({'isFirstLogin': isFirstLogin});
|
||||||
|
result.addAll({'profileImagePath': profileImagePath});
|
||||||
|
result.addAll({'deviceInfo': deviceInfo.toMap()});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AuthenticationState.fromMap(Map<String, dynamic> map) {
|
factory AuthenticationState.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -60,6 +87,11 @@ class AuthenticationState {
|
|||||||
userId: map['userId'] ?? '',
|
userId: map['userId'] ?? '',
|
||||||
userEmail: map['userEmail'] ?? '',
|
userEmail: map['userEmail'] ?? '',
|
||||||
isAuthenticated: map['isAuthenticated'] ?? false,
|
isAuthenticated: map['isAuthenticated'] ?? false,
|
||||||
|
firstName: map['firstName'] ?? '',
|
||||||
|
lastName: map['lastName'] ?? '',
|
||||||
|
isAdmin: map['isAdmin'] ?? false,
|
||||||
|
isFirstLogin: map['isFirstLogin'] ?? false,
|
||||||
|
profileImagePath: map['profileImagePath'] ?? '',
|
||||||
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
|
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,6 +110,11 @@ class AuthenticationState {
|
|||||||
other.userId == userId &&
|
other.userId == userId &&
|
||||||
other.userEmail == userEmail &&
|
other.userEmail == userEmail &&
|
||||||
other.isAuthenticated == isAuthenticated &&
|
other.isAuthenticated == isAuthenticated &&
|
||||||
|
other.firstName == firstName &&
|
||||||
|
other.lastName == lastName &&
|
||||||
|
other.isAdmin == isAdmin &&
|
||||||
|
other.isFirstLogin == isFirstLogin &&
|
||||||
|
other.profileImagePath == profileImagePath &&
|
||||||
other.deviceInfo == deviceInfo;
|
other.deviceInfo == deviceInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +125,11 @@ class AuthenticationState {
|
|||||||
userId.hashCode ^
|
userId.hashCode ^
|
||||||
userEmail.hashCode ^
|
userEmail.hashCode ^
|
||||||
isAuthenticated.hashCode ^
|
isAuthenticated.hashCode ^
|
||||||
|
firstName.hashCode ^
|
||||||
|
lastName.hashCode ^
|
||||||
|
isAdmin.hashCode ^
|
||||||
|
isFirstLogin.hashCode ^
|
||||||
|
profileImagePath.hashCode ^
|
||||||
deviceInfo.hashCode;
|
deviceInfo.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,31 +4,58 @@ class LogInReponse {
|
|||||||
final String accessToken;
|
final String accessToken;
|
||||||
final String userId;
|
final String userId;
|
||||||
final String userEmail;
|
final String userEmail;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
|
final String profileImagePath;
|
||||||
|
final bool isAdmin;
|
||||||
|
final bool isFirstLogin;
|
||||||
|
|
||||||
LogInReponse({
|
LogInReponse({
|
||||||
required this.accessToken,
|
required this.accessToken,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.userEmail,
|
required this.userEmail,
|
||||||
|
required this.firstName,
|
||||||
|
required this.lastName,
|
||||||
|
required this.profileImagePath,
|
||||||
|
required this.isAdmin,
|
||||||
|
required this.isFirstLogin,
|
||||||
});
|
});
|
||||||
|
|
||||||
LogInReponse copyWith({
|
LogInReponse copyWith({
|
||||||
String? accessToken,
|
String? accessToken,
|
||||||
String? userId,
|
String? userId,
|
||||||
String? userEmail,
|
String? userEmail,
|
||||||
|
String? firstName,
|
||||||
|
String? lastName,
|
||||||
|
String? profileImagePath,
|
||||||
|
bool? isAdmin,
|
||||||
|
bool? isFirstLogin,
|
||||||
}) {
|
}) {
|
||||||
return LogInReponse(
|
return LogInReponse(
|
||||||
accessToken: accessToken ?? this.accessToken,
|
accessToken: accessToken ?? this.accessToken,
|
||||||
userId: userId ?? this.userId,
|
userId: userId ?? this.userId,
|
||||||
userEmail: userEmail ?? this.userEmail,
|
userEmail: userEmail ?? this.userEmail,
|
||||||
|
firstName: firstName ?? this.firstName,
|
||||||
|
lastName: lastName ?? this.lastName,
|
||||||
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
|
isAdmin: isAdmin ?? this.isAdmin,
|
||||||
|
isFirstLogin: isFirstLogin ?? this.isFirstLogin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
final result = <String, dynamic>{};
|
||||||
'accessToken': accessToken,
|
|
||||||
'userId': userId,
|
result.addAll({'accessToken': accessToken});
|
||||||
'userEmail': userEmail,
|
result.addAll({'userId': userId});
|
||||||
};
|
result.addAll({'userEmail': userEmail});
|
||||||
|
result.addAll({'firstName': firstName});
|
||||||
|
result.addAll({'lastName': lastName});
|
||||||
|
result.addAll({'profileImagePath': profileImagePath});
|
||||||
|
result.addAll({'isAdmin': isAdmin});
|
||||||
|
result.addAll({'isFirstLogin': isFirstLogin});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory LogInReponse.fromMap(Map<String, dynamic> map) {
|
factory LogInReponse.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -36,6 +63,11 @@ class LogInReponse {
|
|||||||
accessToken: map['accessToken'] ?? '',
|
accessToken: map['accessToken'] ?? '',
|
||||||
userId: map['userId'] ?? '',
|
userId: map['userId'] ?? '',
|
||||||
userEmail: map['userEmail'] ?? '',
|
userEmail: map['userEmail'] ?? '',
|
||||||
|
firstName: map['firstName'] ?? '',
|
||||||
|
lastName: map['lastName'] ?? '',
|
||||||
|
profileImagePath: map['profileImagePath'] ?? '',
|
||||||
|
isAdmin: map['isAdmin'] ?? false,
|
||||||
|
isFirstLogin: map['isFirstLogin'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +76,9 @@ class LogInReponse {
|
|||||||
factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source));
|
factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail)';
|
String toString() {
|
||||||
|
return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, isFirstLogin: $isFirstLogin)';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
@@ -53,9 +87,23 @@ class LogInReponse {
|
|||||||
return other is LogInReponse &&
|
return other is LogInReponse &&
|
||||||
other.accessToken == accessToken &&
|
other.accessToken == accessToken &&
|
||||||
other.userId == userId &&
|
other.userId == userId &&
|
||||||
other.userEmail == userEmail;
|
other.userEmail == userEmail &&
|
||||||
|
other.firstName == firstName &&
|
||||||
|
other.lastName == lastName &&
|
||||||
|
other.profileImagePath == profileImagePath &&
|
||||||
|
other.isAdmin == isAdmin &&
|
||||||
|
other.isFirstLogin == isFirstLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => accessToken.hashCode ^ userId.hashCode ^ userEmail.hashCode;
|
int get hashCode {
|
||||||
|
return accessToken.hashCode ^
|
||||||
|
userId.hashCode ^
|
||||||
|
userEmail.hashCode ^
|
||||||
|
firstName.hashCode ^
|
||||||
|
lastName.hashCode ^
|
||||||
|
profileImagePath.hashCode ^
|
||||||
|
isAdmin.hashCode ^
|
||||||
|
isFirstLogin.hashCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
deviceType: "",
|
deviceType: "",
|
||||||
isAuthenticated: false,
|
|
||||||
userId: "",
|
userId: "",
|
||||||
userEmail: "",
|
userEmail: "",
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
profileImagePath: '',
|
||||||
|
isAdmin: false,
|
||||||
|
isFirstLogin: false,
|
||||||
|
isAuthenticated: false,
|
||||||
deviceInfo: DeviceInfoRemote(
|
deviceInfo: DeviceInfoRemote(
|
||||||
id: 0,
|
id: 0,
|
||||||
userId: "",
|
userId: "",
|
||||||
@@ -76,6 +81,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
userId: payload.userId,
|
userId: payload.userId,
|
||||||
userEmail: payload.userEmail,
|
userEmail: payload.userEmail,
|
||||||
|
firstName: payload.firstName,
|
||||||
|
lastName: payload.lastName,
|
||||||
|
profileImagePath: payload.profileImagePath,
|
||||||
|
isAdmin: payload.isAdmin,
|
||||||
|
isFirstLoggedIn: payload.isFirstLogin,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSavedLoginInfo) {
|
if (isSavedLoginInfo) {
|
||||||
@@ -114,9 +124,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
state = AuthenticationState(
|
state = AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
deviceType: "",
|
deviceType: "",
|
||||||
isAuthenticated: false,
|
|
||||||
userId: "",
|
userId: "",
|
||||||
userEmail: "",
|
userEmail: "",
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
profileImagePath: '',
|
||||||
|
isFirstLogin: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isAdmin: false,
|
||||||
deviceInfo: DeviceInfoRemote(
|
deviceInfo: DeviceInfoRemote(
|
||||||
id: 0,
|
id: 0,
|
||||||
userId: "",
|
userId: "",
|
||||||
@@ -139,6 +154,10 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
|
DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
|
||||||
state = state.copyWith(deviceInfo: deviceInfoRemote);
|
state = state.copyWith(deviceInfo: deviceInfoRemote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUserProfileImagePath(String path) {
|
||||||
|
state = state.copyWith(profileImagePath: path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,8 +2,15 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/upload_profile_image_repsonse.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/user_info.model.dart';
|
import 'package:immich_mobile/shared/models/user_info.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
||||||
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
final NetworkService _networkService = NetworkService();
|
final NetworkService _networkService = NetworkService();
|
||||||
@@ -21,4 +28,39 @@ class UserService {
|
|||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<UploadProfileImageResponse?> uploadProfileImage(XFile image) async {
|
||||||
|
var dio = Dio();
|
||||||
|
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
||||||
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
|
var mimeType = FileHelper.getMimeType(image.path);
|
||||||
|
|
||||||
|
final imageData = MultipartFile.fromBytes(
|
||||||
|
await image.readAsBytes(),
|
||||||
|
filename: image.name,
|
||||||
|
contentType: MediaType(
|
||||||
|
mimeType["type"],
|
||||||
|
mimeType["subType"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final formData = FormData.fromMap({'file': imageData});
|
||||||
|
|
||||||
|
try {
|
||||||
|
Response res = await dio.post(
|
||||||
|
'$savedEndpoint/user/profile-image',
|
||||||
|
data: formData,
|
||||||
|
);
|
||||||
|
|
||||||
|
var payload = UploadProfileImageResponse.fromJson(res.toString());
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
} on DioError catch (e) {
|
||||||
|
debugPrint("Error uploading file: ${e.response}");
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error uploading file: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,28 +7,28 @@ packages:
|
|||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "34.0.0"
|
version: "38.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.4.1"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.11"
|
version: "3.3.0"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.1"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -42,14 +42,14 @@ packages:
|
|||||||
name: auto_route
|
name: auto_route
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.2"
|
version: "4.0.1"
|
||||||
auto_route_generator:
|
auto_route_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: auto_route_generator
|
name: auto_route_generator
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "4.0.0"
|
||||||
badges:
|
badges:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -70,7 +70,7 @@ packages:
|
|||||||
name: build
|
name: build
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.3.0"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -84,21 +84,21 @@ packages:
|
|||||||
name: build_daemon
|
name: build_daemon
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.1.0"
|
||||||
build_resolvers:
|
build_resolvers:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_resolvers
|
name: build_resolvers
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.0.8"
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.7"
|
version: "2.1.10"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -119,14 +119,14 @@ packages:
|
|||||||
name: built_value
|
name: built_value
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.4"
|
version: "8.3.0"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: cached_network_image
|
name: cached_network_image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.1"
|
||||||
cached_network_image_platform_interface:
|
cached_network_image_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -168,14 +168,7 @@ packages:
|
|||||||
name: chewie
|
name: chewie
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.3.2"
|
||||||
cli_util:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: cli_util
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.5"
|
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -196,7 +189,7 @@ packages:
|
|||||||
name: collection
|
name: collection
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.16.0"
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -204,13 +197,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.3+1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -231,14 +231,14 @@ packages:
|
|||||||
name: dart_style
|
name: dart_style
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.3"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dio
|
name: dio
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.4"
|
version: "4.0.6"
|
||||||
equatable:
|
equatable:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -259,14 +259,14 @@ packages:
|
|||||||
name: fake_async
|
name: fake_async
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.2.1"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -280,7 +280,7 @@ packages:
|
|||||||
name: fixnum
|
name: fixnum
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -292,7 +292,7 @@ packages:
|
|||||||
name: flutter_blurhash
|
name: flutter_blurhash
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.8"
|
||||||
flutter_cache_manager:
|
flutter_cache_manager:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -306,7 +306,7 @@ packages:
|
|||||||
name: flutter_hooks
|
name: flutter_hooks
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.18.2"
|
version: "0.18.4"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -328,13 +328,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.14.0"
|
version: "0.14.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.6"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_riverpod
|
name: flutter_riverpod
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0-dev.0"
|
version: "2.0.0-dev.7"
|
||||||
flutter_spinkit:
|
flutter_spinkit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -372,7 +379,7 @@ packages:
|
|||||||
name: fluttertoast
|
name: fluttertoast
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.8"
|
version: "8.0.9"
|
||||||
frontend_server_client:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -400,7 +407,7 @@ packages:
|
|||||||
name: hive
|
name: hive
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.2.1"
|
||||||
hive_flutter:
|
hive_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -421,7 +428,7 @@ packages:
|
|||||||
name: hooks_riverpod
|
name: hooks_riverpod
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0-dev.0"
|
version: "2.0.0-dev.7"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -442,7 +449,7 @@ packages:
|
|||||||
name: http_multi_server
|
name: http_multi_server
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.2.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -456,7 +463,42 @@ packages:
|
|||||||
name: image
|
name: image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.3"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.5+3"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.4+13"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.5+5"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -477,14 +519,14 @@ packages:
|
|||||||
name: js
|
name: js
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.3"
|
version: "0.6.4"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: json_annotation
|
name: json_annotation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.4.0"
|
version: "4.5.0"
|
||||||
latlong2:
|
latlong2:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -526,7 +568,7 @@ packages:
|
|||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3"
|
version: "0.1.4"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -547,7 +589,7 @@ packages:
|
|||||||
name: mime
|
name: mime
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.2"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -575,14 +617,14 @@ packages:
|
|||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.2"
|
||||||
package_info_plus_linux:
|
package_info_plus_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_info_plus_linux
|
name: package_info_plus_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.5"
|
||||||
package_info_plus_macos:
|
package_info_plus_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -603,70 +645,70 @@ packages:
|
|||||||
name: package_info_plus_web
|
name: package_info_plus_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.5"
|
||||||
package_info_plus_windows:
|
package_info_plus_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_info_plus_windows
|
name: package_info_plus_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.5"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.8.1"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.0.10"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.11"
|
version: "2.0.14"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_ios
|
name: path_provider_ios
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.9"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.1.6"
|
||||||
path_provider_macos:
|
path_provider_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_macos
|
name: path_provider_macos
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.0.6"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.4"
|
||||||
path_provider_windows:
|
path_provider_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.0.6"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -680,21 +722,21 @@ packages:
|
|||||||
name: percent_indicator
|
name: percent_indicator
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "4.2.2"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.4.0"
|
version: "5.0.0"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: photo_manager
|
name: photo_manager
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.1.0+2"
|
||||||
photo_view:
|
photo_view:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -750,14 +792,14 @@ packages:
|
|||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "6.0.2"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pub_semver
|
name: pub_semver
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
pubspec_parse:
|
pubspec_parse:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -771,14 +813,14 @@ packages:
|
|||||||
name: quiver
|
name: quiver
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1+1"
|
version: "3.1.0"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod
|
name: riverpod
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0-dev.0"
|
version: "2.0.0-dev.7"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -792,7 +834,7 @@ packages:
|
|||||||
name: shelf
|
name: shelf
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
shelf_web_socket:
|
shelf_web_socket:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -811,7 +853,7 @@ packages:
|
|||||||
name: sliver_tools
|
name: sliver_tools
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.5"
|
version: "0.2.6"
|
||||||
socket_io_client:
|
socket_io_client:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -832,21 +874,21 @@ packages:
|
|||||||
name: source_gen
|
name: source_gen
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.2"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_helper
|
name: source_helper
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.2"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.1"
|
version: "1.8.2"
|
||||||
sprintf:
|
sprintf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -860,14 +902,14 @@ packages:
|
|||||||
name: sqflite
|
name: sqflite
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2+1"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_common
|
name: sqflite_common
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.1+1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -909,7 +951,7 @@ packages:
|
|||||||
name: synchronized
|
name: synchronized
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0+2"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -923,7 +965,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.8"
|
version: "0.4.9"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -951,7 +993,7 @@ packages:
|
|||||||
name: typed_data
|
name: typed_data
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.1"
|
||||||
unicode:
|
unicode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -979,49 +1021,56 @@ packages:
|
|||||||
name: uuid
|
name: uuid
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.5"
|
version: "3.0.6"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
|
very_good_analysis:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: very_good_analysis
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
video_player:
|
video_player:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: video_player
|
name: video_player
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.18"
|
version: "2.4.2"
|
||||||
video_player_android:
|
video_player_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_android
|
name: video_player_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.17"
|
version: "2.3.3"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_avfoundation
|
name: video_player_avfoundation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.18"
|
version: "2.3.4"
|
||||||
video_player_platform_interface:
|
video_player_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_platform_interface
|
name: video_player_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.1"
|
version: "5.1.2"
|
||||||
video_player_web:
|
video_player_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_web
|
name: video_player_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.0.10"
|
||||||
visibility_detector:
|
visibility_detector:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1035,7 +1084,7 @@ packages:
|
|||||||
name: wakelock
|
name: wakelock
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.6"
|
version: "0.6.1+2"
|
||||||
wakelock_macos:
|
wakelock_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1077,14 +1126,14 @@ packages:
|
|||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.2.0"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.8"
|
version: "2.5.2"
|
||||||
wkt_parser:
|
wkt_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1098,21 +1147,21 @@ packages:
|
|||||||
name: xdg_directories
|
name: xdg_directories
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.0+1"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.3.1"
|
version: "5.4.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: yaml
|
name: yaml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.15.1 <3.0.0"
|
dart: ">=2.17.0 <3.0.0"
|
||||||
flutter: ">=2.8.0"
|
flutter: ">=3.0.0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.9.0+13
|
version: 1.10.0+15
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.15.1 <3.0.0"
|
sdk: ">=2.15.1 <3.0.0"
|
||||||
@@ -14,13 +14,13 @@ dependencies:
|
|||||||
photo_manager: ^2.0.6
|
photo_manager: ^2.0.6
|
||||||
flutter_hooks: ^0.18.0
|
flutter_hooks: ^0.18.0
|
||||||
hooks_riverpod: ^2.0.0-dev.0
|
hooks_riverpod: ^2.0.0-dev.0
|
||||||
hive:
|
hive: ^2.2.1
|
||||||
hive_flutter:
|
hive_flutter: ^1.1.0
|
||||||
dio: ^4.0.4
|
dio: ^4.0.4
|
||||||
cached_network_image: ^3.2.0
|
cached_network_image: ^3.2.1
|
||||||
percent_indicator: ^3.4.0
|
percent_indicator: ^4.2.2
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
auto_route: ^3.2.2
|
auto_route: ^4.0.1
|
||||||
exif: ^3.1.1
|
exif: ^3.1.1
|
||||||
transparent_image: ^2.0.0
|
transparent_image: ^2.0.0
|
||||||
visibility_detector: ^0.2.2
|
visibility_detector: ^0.2.2
|
||||||
@@ -38,6 +38,7 @@ dependencies:
|
|||||||
flutter_spinkit: ^5.1.0
|
flutter_spinkit: ^5.1.0
|
||||||
flutter_swipe_detector: ^2.0.0
|
flutter_swipe_detector: ^2.0.0
|
||||||
equatable: ^2.0.3
|
equatable: ^2.0.3
|
||||||
|
image_picker: ^0.8.5+3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -45,7 +46,7 @@ dev_dependencies:
|
|||||||
flutter_lints: ^1.0.0
|
flutter_lints: ^1.0.0
|
||||||
hive_generator: ^1.1.2
|
hive_generator: ^1.1.2
|
||||||
build_runner: ^2.1.7
|
build_runner: ^2.1.7
|
||||||
auto_route_generator: ^3.2.1
|
auto_route_generator: ^4.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"printWidth": 120
|
"printWidth": 120,
|
||||||
|
"semi": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN apk add --update-cache build-base python3
|
RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
|||||||
637
server/package-lock.json
generated
637
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@
|
|||||||
"@nestjs/platform-express": "^8.0.0",
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
"@nestjs/platform-fastify": "^8.2.6",
|
"@nestjs/platform-fastify": "^8.2.6",
|
||||||
"@nestjs/platform-socket.io": "^8.2.6",
|
"@nestjs/platform-socket.io": "^8.2.6",
|
||||||
|
"@nestjs/schedule": "^2.0.1",
|
||||||
"@nestjs/typeorm": "^8.0.3",
|
"@nestjs/typeorm": "^8.0.3",
|
||||||
"@nestjs/websockets": "^8.2.6",
|
"@nestjs/websockets": "^8.2.6",
|
||||||
"@socket.io/redis-adapter": "^7.1.0",
|
"@socket.io/redis-adapter": "^7.1.0",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"diskusage": "^1.1.3",
|
"diskusage": "^1.1.3",
|
||||||
"dotenv": "^14.2.0",
|
"dotenv": "^14.2.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"joi": "^17.5.0",
|
"joi": "^17.5.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"passport": "^0.5.2",
|
"passport": "^0.5.2",
|
||||||
@@ -53,6 +55,7 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
|
"sharp": "^0.28.0",
|
||||||
"socket.io-redis": "^6.1.1",
|
"socket.io-redis": "^6.1.1",
|
||||||
"systeminformation": "^5.11.0",
|
"systeminformation": "^5.11.0",
|
||||||
"typeorm": "^0.2.41"
|
"typeorm": "^0.2.41"
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"@nestjs/testing": "^8.0.0",
|
"@nestjs/testing": "^8.0.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bull": "^3.15.7",
|
"@types/bull": "^3.15.7",
|
||||||
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/imagemin": "^8.0.0",
|
"@types/imagemin": "^8.0.0",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
@@ -70,6 +74,7 @@
|
|||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
"@types/passport-jwt": "^3.0.6",
|
"@types/passport-jwt": "^3.0.6",
|
||||||
|
"@types/sharp": "^0.30.2",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
||||||
import { multerOption } from '../../config/multer-option.config';
|
import { assetUploadOption } from '../../config/asset-upload.config';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { ServeFileDto } from './dto/serve-file.dto';
|
import { ServeFileDto } from './dto/serve-file.dto';
|
||||||
@@ -39,7 +39,7 @@ export class AssetController {
|
|||||||
private wsCommunicateionGateway: CommunicationGateway,
|
private wsCommunicateionGateway: CommunicationGateway,
|
||||||
private assetService: AssetService,
|
private assetService: AssetService,
|
||||||
private backgroundTaskService: BackgroundTaskService,
|
private backgroundTaskService: BackgroundTaskService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post('upload')
|
@Post('upload')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
@@ -48,7 +48,7 @@ export class AssetController {
|
|||||||
{ name: 'assetData', maxCount: 1 },
|
{ name: 'assetData', maxCount: 1 },
|
||||||
{ name: 'thumbnailData', maxCount: 1 },
|
{ name: 'thumbnailData', maxCount: 1 },
|
||||||
],
|
],
|
||||||
multerOption,
|
assetUploadOption,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async uploadFile(
|
async uploadFile(
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { MoreThan, Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { AssetEntity, AssetType } from './entities/asset.entity';
|
import { AssetEntity, AssetType } from './entities/asset.entity';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
|
||||||
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
|
|
||||||
import { createReadStream, stat } from 'fs';
|
import { createReadStream, stat } from 'fs';
|
||||||
import { ServeFileDto } from './dto/serve-file.dto';
|
import { ServeFileDto } from './dto/serve-file.dto';
|
||||||
import { Response as Res } from 'express';
|
import { Response as Res } from 'express';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||||
import { SearchAssetDto } from './dto/search-asset.dto';
|
import { SearchAssetDto } from './dto/search-asset.dto';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
@@ -22,7 +19,7 @@ export class AssetService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
public async updateThumbnailInfo(assetId: string, path: string) {
|
public async updateThumbnailInfo(assetId: string, path: string) {
|
||||||
return await this.assetRepository.update(assetId, {
|
return await this.assetRepository.update(assetId, {
|
||||||
@@ -114,34 +111,66 @@ export class AssetService {
|
|||||||
public async getAssetThumbnail(assetId: string) {
|
public async getAssetThumbnail(assetId: string) {
|
||||||
const asset = await this.assetRepository.findOne({ id: assetId });
|
const asset = await this.assetRepository.findOne({ id: assetId });
|
||||||
|
|
||||||
return new StreamableFile(createReadStream(asset.resizePath));
|
if (asset.webpPath != '') {
|
||||||
|
return new StreamableFile(createReadStream(asset.webpPath));
|
||||||
|
} else {
|
||||||
|
return new StreamableFile(createReadStream(asset.resizePath));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
|
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
|
||||||
let file = null;
|
let file = null;
|
||||||
const asset = await this.findOne(query.did, query.aid);
|
const asset = await this.findOne(query.did, query.aid);
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
throw new BadRequestException('Asset does not exist');
|
throw new BadRequestException('Asset does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Handle Sending Images
|
// Handle Sending Images
|
||||||
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
|
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
|
||||||
res.set({
|
/**
|
||||||
'Content-Type': asset.mimeType,
|
* Serve file viewer on the web
|
||||||
});
|
*/
|
||||||
|
if (query.isWeb) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
});
|
||||||
|
return new StreamableFile(createReadStream(asset.resizePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve thumbnail image for both web and mobile app
|
||||||
|
*/
|
||||||
if (query.isThumb === 'false' || !query.isThumb) {
|
if (query.isThumb === 'false' || !query.isThumb) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': asset.mimeType,
|
||||||
|
});
|
||||||
file = createReadStream(asset.originalPath);
|
file = createReadStream(asset.originalPath);
|
||||||
} else {
|
} else {
|
||||||
file = createReadStream(asset.resizePath);
|
if (asset.webpPath != '') {
|
||||||
|
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) => {
|
file.on('error', (error) => {
|
||||||
Logger.log(`Cannot create read stream ${error}`);
|
Logger.log(`Cannot create read stream ${error}`);
|
||||||
return new BadRequestException('Cannot Create Read Stream');
|
return new BadRequestException('Cannot Create Read Stream');
|
||||||
});
|
});
|
||||||
|
|
||||||
return new StreamableFile(file);
|
return new StreamableFile(file);
|
||||||
|
|
||||||
} else if (asset.type == AssetType.VIDEO) {
|
} else if (asset.type == AssetType.VIDEO) {
|
||||||
// Handle Handling Video
|
// Handle Video
|
||||||
const { size } = await fileInfo(asset.originalPath);
|
const { size } = await fileInfo(asset.originalPath);
|
||||||
const range = headers.range;
|
const range = headers.range;
|
||||||
|
|
||||||
@@ -183,6 +212,8 @@ export class AssetService {
|
|||||||
const videoStream = createReadStream(asset.originalPath, { start: start, end: end });
|
const videoStream = createReadStream(asset.originalPath, { start: start, end: end });
|
||||||
|
|
||||||
return new StreamableFile(videoStream);
|
return new StreamableFile(videoStream);
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': asset.mimeType,
|
'Content-Type': asset.mimeType,
|
||||||
|
|||||||
@@ -13,4 +13,8 @@ export class ServeFileDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBooleanString()
|
@IsBooleanString()
|
||||||
isThumb: string;
|
isThumb: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBooleanString()
|
||||||
|
isWeb: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export class AssetEntity {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
resizePath: string;
|
resizePath: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
webpPath: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import { SignUpDto } from './dto/sign-up.dto';
|
|||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) { }
|
||||||
|
|
||||||
@Post('/login')
|
@Post('/login')
|
||||||
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) {
|
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) {
|
||||||
return await this.authService.login(loginCredential);
|
return await this.authService.login(loginCredential);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/signUp')
|
@Post('/admin-sign-up')
|
||||||
async signUp(@Body(ValidationPipe) signUpCrendential: SignUpDto) {
|
async adminSignUp(@Body(ValidationPipe) signUpCrendential: SignUpDto) {
|
||||||
return await this.authService.signUp(signUpCrendential);
|
return await this.authService.adminSignUp(signUpCrendential);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ export class AuthService {
|
|||||||
@InjectRepository(UserEntity)
|
@InjectRepository(UserEntity)
|
||||||
private userRepository: Repository<UserEntity>,
|
private userRepository: Repository<UserEntity>,
|
||||||
private immichJwtService: ImmichJwtService,
|
private immichJwtService: ImmichJwtService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
private async validateUser(loginCredential: LoginCredentialDto): Promise<UserEntity> {
|
private async validateUser(loginCredential: LoginCredentialDto): Promise<UserEntity> {
|
||||||
const user = await this.userRepository.findOne(
|
const user = await this.userRepository.findOne(
|
||||||
{ email: loginCredential.email },
|
{ email: loginCredential.email },
|
||||||
{ select: ['id', 'email', 'password', 'salt'] },
|
{ select: ['id', 'email', 'password', 'salt', 'firstName', 'lastName', 'isAdmin', 'profileImagePath', 'isFirstLoggedIn'] },
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAuthenticated = await this.validatePassword(user.password, loginCredential.password, user.salt);
|
const isAuthenticated = await this.validatePassword(user.password, loginCredential.password, user.salt);
|
||||||
@@ -44,32 +44,45 @@ export class AuthService {
|
|||||||
accessToken: await this.immichJwtService.generateToken(payload),
|
accessToken: await this.immichJwtService.generateToken(payload),
|
||||||
userId: validatedUser.id,
|
userId: validatedUser.id,
|
||||||
userEmail: validatedUser.email,
|
userEmail: validatedUser.email,
|
||||||
|
firstName: validatedUser.firstName,
|
||||||
|
lastName: validatedUser.lastName,
|
||||||
|
isAdmin: validatedUser.isAdmin,
|
||||||
|
profileImagePath: validatedUser.profileImagePath,
|
||||||
|
isFirstLogin: validatedUser.isFirstLoggedIn
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async signUp(signUpCrendential: SignUpDto) {
|
|
||||||
const registerUser = await this.userRepository.findOne({ email: signUpCrendential.email });
|
|
||||||
|
|
||||||
if (registerUser) {
|
public async adminSignUp(signUpCrendential: SignUpDto) {
|
||||||
throw new BadRequestException('User exist');
|
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
|
||||||
|
|
||||||
|
if (adminUser) {
|
||||||
|
throw new BadRequestException('The server already has an admin')
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = new UserEntity();
|
|
||||||
newUser.email = signUpCrendential.email;
|
const newAdminUser = new UserEntity();
|
||||||
newUser.salt = await bcrypt.genSalt();
|
newAdminUser.email = signUpCrendential.email;
|
||||||
newUser.password = await this.hashPassword(signUpCrendential.password, newUser.salt);
|
newAdminUser.salt = await bcrypt.genSalt();
|
||||||
|
newAdminUser.password = await this.hashPassword(signUpCrendential.password, newAdminUser.salt);
|
||||||
|
newAdminUser.firstName = signUpCrendential.firstName;
|
||||||
|
newAdminUser.lastName = signUpCrendential.lastName;
|
||||||
|
newAdminUser.isAdmin = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedUser = await this.userRepository.save(newUser);
|
const savedNewAdminUserUser = await this.userRepository.save(newAdminUser);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: savedUser.id,
|
id: savedNewAdminUserUser.id,
|
||||||
email: savedUser.email,
|
email: savedNewAdminUserUser.email,
|
||||||
createdAt: savedUser.createdAt,
|
firstName: savedNewAdminUserUser.firstName,
|
||||||
|
lastName: savedNewAdminUserUser.lastName,
|
||||||
|
createdAt: savedNewAdminUserUser.createdAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error('e', 'signUp');
|
Logger.error('e', 'signUp');
|
||||||
throw new InternalServerErrorException('Failed to register new user');
|
throw new InternalServerErrorException('Failed to register new admin user');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,10 @@ export class SignUpDto {
|
|||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,27 @@
|
|||||||
export class CreateUserDto {}
|
import { IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
profileImagePath: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
isAdmin: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
isFirstLoggedIn: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ export class UserEntity {
|
|||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
isAdmin: boolean;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@@ -14,6 +23,12 @@ export class UserEntity {
|
|||||||
@Column({ select: false })
|
@Column({ select: false })
|
||||||
salt: string;
|
salt: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
profileImagePath: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
isFirstLoggedIn: boolean;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,54 @@
|
|||||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe, Put, Query, UseInterceptors, UploadedFile, Response } from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
|
import { AdminRolesGuard } from '../../middlewares/admin-role-guard.middleware';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import { profileImageUploadOption } from '../../config/profile-image-upload.config';
|
||||||
|
import { Response as Res } from 'express';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Controller('user')
|
@Controller('user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(private readonly userService: UserService) {}
|
constructor(private readonly userService: UserService) { }
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get()
|
@Get()
|
||||||
async getAllUsers(@GetAuthUser() authUser: AuthUserDto) {
|
async getAllUsers(@GetAuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean) {
|
||||||
return await this.userService.getAllUsers(authUser);
|
return await this.userService.getAllUsers(authUser, isAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@UseGuards(AdminRolesGuard)
|
||||||
|
@Post()
|
||||||
|
async createNewUser(@Body(ValidationPipe) createUserDto: CreateUserDto) {
|
||||||
|
return await this.userService.createUser(createUserDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/count')
|
||||||
|
async getUserCount(@Query('isAdmin') isAdmin: boolean) {
|
||||||
|
|
||||||
|
return await this.userService.getUserCount(isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Put()
|
||||||
|
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) {
|
||||||
|
return await this.userService.updateUser(updateUserDto)
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@UseInterceptors(FileInterceptor('file', profileImageUploadOption))
|
||||||
|
@Post('/profile-image')
|
||||||
|
async createProfileImage(@GetAuthUser() authUser: AuthUserDto, @UploadedFile() fileInfo: Express.Multer.File) {
|
||||||
|
return await this.userService.createProfileImage(authUser, fileInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/profile-image/:userId')
|
||||||
|
async getProfileImage(@Param('userId') userId: string,
|
||||||
|
@Response({ passthrough: true }) res: Res,
|
||||||
|
) {
|
||||||
|
return await this.userService.getUserProfileImage(userId, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import { UserService } from './user.service';
|
|||||||
import { UserController } from './user.controller';
|
import { UserController } from './user.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { UserEntity } from './entities/user.entity';
|
import { UserEntity } from './entities/user.entity';
|
||||||
|
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
|
||||||
|
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { jwtConfig } from '../../config/jwt.config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([UserEntity])],
|
imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [UserService],
|
providers: [UserService, ImmichJwtService],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule { }
|
||||||
|
|||||||
@@ -1,21 +1,166 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Not, Repository } from 'typeorm';
|
import { Not, Repository } from 'typeorm';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
import { UpdateUserDto } from './dto/update-user.dto';
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
import { UserEntity } from './entities/user.entity';
|
import { UserEntity } from './entities/user.entity';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { createReadStream, unlink, unlinkSync } from 'fs';
|
||||||
|
import { Response as Res } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(UserEntity)
|
@InjectRepository(UserEntity)
|
||||||
private userRepository: Repository<UserEntity>,
|
private userRepository: Repository<UserEntity>,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
|
async getAllUsers(authUser: AuthUserDto, isAll: boolean) {
|
||||||
|
|
||||||
|
if (isAll) {
|
||||||
|
return await this.userRepository.find();
|
||||||
|
}
|
||||||
|
|
||||||
async getAllUsers(authUser: AuthUserDto) {
|
|
||||||
return await this.userRepository.find({
|
return await this.userRepository.find({
|
||||||
where: { id: Not(authUser.id) },
|
where: { id: Not(authUser.id) },
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserCount(isAdmin: boolean) {
|
||||||
|
let users;
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
users = await this.userRepository.find({ where: { isAdmin: true } });
|
||||||
|
} else {
|
||||||
|
users = await this.userRepository.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
userCount: users.length
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(createUserDto: CreateUserDto) {
|
||||||
|
const user = await this.userRepository.findOne({ where: { email: createUserDto.email } });
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
throw new BadRequestException('User exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = new UserEntity();
|
||||||
|
newUser.email = createUserDto.email;
|
||||||
|
newUser.salt = await bcrypt.genSalt();
|
||||||
|
newUser.password = await this.hashPassword(createUserDto.password, newUser.salt);
|
||||||
|
newUser.firstName = createUserDto.firstName;
|
||||||
|
newUser.lastName = createUserDto.lastName;
|
||||||
|
newUser.isAdmin = false;
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const savedUser = await this.userRepository.save(newUser);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: savedUser.id,
|
||||||
|
email: savedUser.email,
|
||||||
|
firstName: savedUser.firstName,
|
||||||
|
lastName: savedUser.lastName,
|
||||||
|
createdAt: savedUser.createdAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(e, 'Create new user');
|
||||||
|
throw new InternalServerErrorException('Failed to register new user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hashPassword(password: string, salt: string): Promise<string> {
|
||||||
|
return bcrypt.hash(password, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async updateUser(updateUserDto: UpdateUserDto) {
|
||||||
|
const user = await this.userRepository.findOne(updateUserDto.id);
|
||||||
|
|
||||||
|
user.lastName = updateUserDto.lastName || user.lastName;
|
||||||
|
user.firstName = updateUserDto.firstName || user.firstName;
|
||||||
|
user.profileImagePath = updateUserDto.profileImagePath || user.profileImagePath;
|
||||||
|
user.isFirstLoggedIn = updateUserDto.isFirstLoggedIn || user.isFirstLoggedIn;
|
||||||
|
|
||||||
|
// If payload includes password - Create new password for user
|
||||||
|
if (updateUserDto.password) {
|
||||||
|
user.salt = await bcrypt.genSalt();
|
||||||
|
user.password = await this.hashPassword(updateUserDto.password, user.salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateUserDto.isAdmin) {
|
||||||
|
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } })
|
||||||
|
|
||||||
|
if (adminUser) {
|
||||||
|
throw new BadRequestException("Admin user exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
user.isAdmin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedUser = await this.userRepository.save(user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updatedUser.id,
|
||||||
|
email: updatedUser.email,
|
||||||
|
firstName: updatedUser.firstName,
|
||||||
|
lastName: updatedUser.lastName,
|
||||||
|
isAdmin: updatedUser.isAdmin,
|
||||||
|
profileImagePath: updatedUser.profileImagePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(e, 'Create new user');
|
||||||
|
throw new InternalServerErrorException('Failed to register new user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProfileImage(authUser: AuthUserDto, fileInfo: Express.Multer.File) {
|
||||||
|
try {
|
||||||
|
await this.userRepository.update(authUser.id, {
|
||||||
|
profileImagePath: fileInfo.path
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: authUser.id,
|
||||||
|
profileImagePath: fileInfo.path
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(e, 'Create User Profile Image');
|
||||||
|
throw new InternalServerErrorException('Failed to create new user profile image');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserProfileImage(userId: string, res: Res) {
|
||||||
|
try {
|
||||||
|
const user = await this.userRepository.findOne({ id: userId })
|
||||||
|
if (!user.profileImagePath) {
|
||||||
|
console.log("empty return")
|
||||||
|
throw new BadRequestException('User does not have a profile image');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileStream = createReadStream(user.profileImagePath)
|
||||||
|
return new StreamableFile(fileStream);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error getting user profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
server/src/app.controller.ts
Normal file
15
server/src/app.controller.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Controller, Get, Res, Headers } from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
|
||||||
|
export class AppController {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async redirectToWebpage(@Res({ passthrough: true }) res: Response, @Headers() headers) {
|
||||||
|
const host = headers.host;
|
||||||
|
|
||||||
|
return res.redirect(`http://${host}:2285`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,20 +15,31 @@ import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
|||||||
import { BackgroundTaskModule } from './modules/background-task/background-task.module';
|
import { BackgroundTaskModule } from './modules/background-task/background-task.module';
|
||||||
import { CommunicationModule } from './api-v1/communication/communication.module';
|
import { CommunicationModule } from './api-v1/communication/communication.module';
|
||||||
import { SharingModule } from './api-v1/sharing/sharing.module';
|
import { SharingModule } from './api-v1/sharing/sharing.module';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot(immichAppConfig),
|
ConfigModule.forRoot(immichAppConfig),
|
||||||
|
|
||||||
TypeOrmModule.forRoot(databaseConfig),
|
TypeOrmModule.forRoot(databaseConfig),
|
||||||
|
|
||||||
UserModule,
|
UserModule,
|
||||||
|
|
||||||
AssetModule,
|
AssetModule,
|
||||||
|
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
|
||||||
ImmichJwtModule,
|
ImmichJwtModule,
|
||||||
|
|
||||||
DeviceInfoModule,
|
DeviceInfoModule,
|
||||||
|
|
||||||
BullModule.forRootAsync({
|
BullModule.forRootAsync({
|
||||||
useFactory: async () => ({
|
useFactory: async () => ({
|
||||||
redis: {
|
redis: {
|
||||||
host: 'immich_redis',
|
host: process.env.REDIS_HOSTNAME || 'immich_redis',
|
||||||
port: 6379,
|
port: 6379,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -43,8 +54,12 @@ import { SharingModule } from './api-v1/sharing/sharing.module';
|
|||||||
CommunicationModule,
|
CommunicationModule,
|
||||||
|
|
||||||
SharingModule,
|
SharingModule,
|
||||||
|
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
|
||||||
|
ScheduleTasksModule
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [AppController],
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ export const immichAppConfig: ConfigModuleOptions = {
|
|||||||
then: Joi.string().optional().allow(null, ''),
|
then: Joi.string().optional().allow(null, ''),
|
||||||
otherwise: Joi.string().required(),
|
otherwise: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
|
VITE_SERVER_ENDPOINT: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant';
|
|||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { CreateAssetDto } from '../api-v1/asset/dto/create-asset.dto';
|
import { CreateAssetDto } from '../api-v1/asset/dto/create-asset.dto';
|
||||||
|
|
||||||
export const multerOption: MulterOptions = {
|
export const assetUploadOption: MulterOptions = {
|
||||||
fileFilter: (req: Request, file: any, cb: any) => {
|
fileFilter: (req: Request, file: any, cb: any) => {
|
||||||
if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng|webp)$/)) {
|
if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng|webp)$/)) {
|
||||||
cb(null, true);
|
cb(null, true);
|
||||||
@@ -19,14 +19,14 @@ export const multerOption: MulterOptions = {
|
|||||||
|
|
||||||
storage: diskStorage({
|
storage: diskStorage({
|
||||||
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||||
const uploadPath = APP_UPLOAD_LOCATION;
|
const basePath = APP_UPLOAD_LOCATION;
|
||||||
const fileInfo = req.body as CreateAssetDto;
|
const fileInfo = req.body as CreateAssetDto;
|
||||||
|
|
||||||
const yearInfo = new Date(fileInfo.createdAt).getFullYear();
|
const yearInfo = new Date(fileInfo.createdAt).getFullYear();
|
||||||
const monthInfo = new Date(fileInfo.createdAt).getMonth();
|
const monthInfo = new Date(fileInfo.createdAt).getMonth();
|
||||||
|
|
||||||
if (file.fieldname == 'assetData') {
|
if (file.fieldname == 'assetData') {
|
||||||
const originalUploadFolder = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
|
const originalUploadFolder = `${basePath}/${req.user['id']}/original/${req.body['deviceId']}`;
|
||||||
|
|
||||||
if (!existsSync(originalUploadFolder)) {
|
if (!existsSync(originalUploadFolder)) {
|
||||||
mkdirSync(originalUploadFolder, { recursive: true });
|
mkdirSync(originalUploadFolder, { recursive: true });
|
||||||
@@ -35,7 +35,7 @@ export const multerOption: MulterOptions = {
|
|||||||
// Save original to disk
|
// Save original to disk
|
||||||
cb(null, originalUploadFolder);
|
cb(null, originalUploadFolder);
|
||||||
} else if (file.fieldname == 'thumbnailData') {
|
} else if (file.fieldname == 'thumbnailData') {
|
||||||
const thumbnailUploadFolder = `${uploadPath}/${req.user['id']}/thumb/${req.body['deviceId']}`;
|
const thumbnailUploadFolder = `${basePath}/${req.user['id']}/thumb/${req.body['deviceId']}`;
|
||||||
|
|
||||||
if (!existsSync(thumbnailUploadFolder)) {
|
if (!existsSync(thumbnailUploadFolder)) {
|
||||||
mkdirSync(thumbnailUploadFolder, { recursive: true });
|
mkdirSync(thumbnailUploadFolder, { recursive: true });
|
||||||
@@ -56,3 +56,5 @@ export const multerOption: MulterOptions = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
export const databaseConfig: TypeOrmModuleOptions = {
|
export const databaseConfig: TypeOrmModuleOptions = {
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: 'immich_postgres',
|
host: process.env.DB_HOSTNAME || 'immich_postgres',
|
||||||
port: 5432,
|
port: 5432,
|
||||||
username: process.env.DB_USERNAME,
|
username: process.env.DB_USERNAME,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
|
|||||||
40
server/src/config/profile-image-upload.config.ts
Normal file
40
server/src/config/profile-image-upload.config.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||||
|
import { existsSync, mkdirSync } from 'fs';
|
||||||
|
import { diskStorage } from 'multer';
|
||||||
|
import { extname } from 'path';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant';
|
||||||
|
|
||||||
|
export const profileImageUploadOption: MulterOptions = {
|
||||||
|
fileFilter: (req: Request, file: any, cb: any) => {
|
||||||
|
if (file.mimetype.match(/\/(jpg|jpeg|png|heic|heif|dng|webp)$/)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||||
|
const basePath = APP_UPLOAD_LOCATION;
|
||||||
|
const profileImageLocation = `${basePath}/${req.user['id']}/profile`;
|
||||||
|
|
||||||
|
if (!existsSync(profileImageLocation)) {
|
||||||
|
mkdirSync(profileImageLocation, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cb(null, profileImageLocation);
|
||||||
|
},
|
||||||
|
|
||||||
|
filename: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||||
|
|
||||||
|
const userId = req.user['id'];
|
||||||
|
|
||||||
|
cb(null, `${userId}${extname(file.originalname)}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
export const serverVersion = {
|
export const serverVersion = {
|
||||||
major: 1,
|
major: 1,
|
||||||
minor: 9,
|
minor: 10,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
build: 13,
|
build: 14,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||||
import { UserEntity } from '../api-v1/user/entities/user.entity';
|
import { UserEntity } from '../api-v1/user/entities/user.entity';
|
||||||
// import { AuthUserDto } from './dto/auth-user.dto';
|
// import { AuthUserDto } from './dto/auth-user.dto';
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
|
|
||||||
|
app.enableCors();
|
||||||
|
|
||||||
app.set('trust proxy');
|
app.set('trust proxy');
|
||||||
|
|
||||||
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||||
|
|||||||
30
server/src/middlewares/admin-role-guard.middleware.ts
Normal file
30
server/src/middlewares/admin-role-guard.middleware.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { UserEntity } from '../api-v1/user/entities/user.entity';
|
||||||
|
import { ImmichJwtService } from '../modules/immich-jwt/immich-jwt.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminRolesGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector, private jwtService: ImmichJwtService,
|
||||||
|
@InjectRepository(UserEntity)
|
||||||
|
private userRepository: Repository<UserEntity>,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
if (request.headers['authorization']) {
|
||||||
|
const bearerToken = request.headers['authorization'].split(" ")[1]
|
||||||
|
const { userId } = await this.jwtService.validateToken(bearerToken);
|
||||||
|
|
||||||
|
const user = await this.userRepository.findOne(userId);
|
||||||
|
|
||||||
|
return user.isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@ import { RedisClient } from 'redis';
|
|||||||
import { ServerOptions } from 'socket.io';
|
import { ServerOptions } from 'socket.io';
|
||||||
import { createAdapter } from 'socket.io-redis';
|
import { createAdapter } from 'socket.io-redis';
|
||||||
|
|
||||||
// const pubClient = createClient({ url: 'redis://immich_redis:6379' });
|
const redis_host = process.env.REDIS_HOSTNAME || 'immich_redis'
|
||||||
|
// const pubClient = createClient({ url: `redis://${redis_host}:6379` });
|
||||||
// const subClient = pubClient.duplicate();
|
// const subClient = pubClient.duplicate();
|
||||||
|
|
||||||
const pubClient = new RedisClient({
|
const pubClient = new RedisClient({
|
||||||
|
host: redis_host,
|
||||||
port: 6379,
|
port: 6379,
|
||||||
host: 'immich_redis',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const subClient = pubClient.duplicate();
|
const subClient = pubClient.duplicate();
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class UpdateUserTableWithAdminAndName1652633525943 implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
alter table users
|
||||||
|
add column if not exists "firstName" varchar default '';
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add column if not exists "lastName" varchar default '';
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add column if not exists "profileImagePath" varchar default '';
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add column if not exists "isAdmin" bool default false;
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add column if not exists "isFirstLoggedIn" bool default true;
|
||||||
|
`)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
alter table users
|
||||||
|
drop column "firstName";
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
drop column "lastName";
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
drop column "isAdmin";
|
||||||
|
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class UpdateAssetTableWithWebpPath1653214255670 implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
alter table assets
|
||||||
|
add column if not exists "webpPath" varchar default '';
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
alter table assets
|
||||||
|
drop column if exists "webpPath";
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -115,7 +115,7 @@ export class BackgroundTaskProcessor {
|
|||||||
async tagImage(job) {
|
async tagImage(job) {
|
||||||
const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data;
|
const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data;
|
||||||
|
|
||||||
const res = await axios.post('http://immich_microservices:3001/image-classifier/tagImage', {
|
const res = await axios.post('http://immich-microservices:3001/image-classifier/tagImage', {
|
||||||
thumbnailPath: thumbnailPath,
|
thumbnailPath: thumbnailPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,19 +132,24 @@ export class BackgroundTaskProcessor {
|
|||||||
|
|
||||||
@Process('detect-object')
|
@Process('detect-object')
|
||||||
async detectObject(job) {
|
async detectObject(job) {
|
||||||
const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data;
|
try {
|
||||||
|
const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data;
|
||||||
|
|
||||||
const res = await axios.post('http://immich_microservices:3001/object-detection/detectObject', {
|
const res = await axios.post('http://immich-microservices:3001/object-detection/detectObject', {
|
||||||
thumbnailPath: thumbnailPath,
|
thumbnailPath: thumbnailPath,
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status == 201 && res.data.length > 0) {
|
|
||||||
const smartInfo = new SmartInfoEntity();
|
|
||||||
smartInfo.assetId = asset.id;
|
|
||||||
smartInfo.objects = [...res.data];
|
|
||||||
await this.smartInfoRepository.upsert(smartInfo, {
|
|
||||||
conflictPaths: ['assetId'],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.status == 201 && res.data.length > 0) {
|
||||||
|
const smartInfo = new SmartInfoEntity();
|
||||||
|
smartInfo.assetId = asset.id;
|
||||||
|
smartInfo.objects = [...res.data];
|
||||||
|
await this.smartInfoRepository.upsert(smartInfo, {
|
||||||
|
conflictPaths: ['assetId'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`Failed to trigger object detection pipe line ${error.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class BackgroundTaskService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectQueue('background-task')
|
@InjectQueue('background-task')
|
||||||
private backgroundTaskQueue: Queue,
|
private backgroundTaskQueue: Queue,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
async extractExif(savedAsset: AssetEntity, fileName: string, fileSize: number) {
|
async extractExif(savedAsset: AssetEntity, fileName: string, fileSize: number) {
|
||||||
await this.backgroundTaskQueue.add(
|
await this.backgroundTaskQueue.add(
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ImageConversionService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AssetEntity)
|
||||||
|
private assetRepository: Repository<AssetEntity>
|
||||||
|
) { }
|
||||||
|
|
||||||
|
@Cron(CronExpression.EVERY_5_MINUTES
|
||||||
|
, {
|
||||||
|
name: 'webp-conversion'
|
||||||
|
})
|
||||||
|
async webpConversion() {
|
||||||
|
Logger.log('Starting Webp Conversion Tasks', 'ImageConversionService')
|
||||||
|
|
||||||
|
const assets = await this.assetRepository.find({
|
||||||
|
where: {
|
||||||
|
webpPath: ''
|
||||||
|
},
|
||||||
|
take: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (assets.length == 0) {
|
||||||
|
Logger.log('All assets has webp file - aborting task', 'ImageConversionService')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
const resizePath = asset.resizePath;
|
||||||
|
if (resizePath != '') {
|
||||||
|
const webpPath = resizePath.replace('jpeg', 'webp')
|
||||||
|
|
||||||
|
sharp(resizePath).resize(250).webp().toFile(webpPath, (err, info) => {
|
||||||
|
|
||||||
|
if (!err) {
|
||||||
|
this.assetRepository.update({ id: asset.id }, { webpPath: webpPath })
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
server/src/modules/schedule-tasks/schedule-tasks.module.ts
Normal file
12
server/src/modules/schedule-tasks/schedule-tasks.module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
||||||
|
import { ImageConversionService } from './image-conversion.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([AssetEntity]),
|
||||||
|
],
|
||||||
|
providers: [ImageConversionService],
|
||||||
|
})
|
||||||
|
export class ScheduleTasksModule { }
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { INestApplication } from '@nestjs/common';
|
|
||||||
import * as request from 'supertest';
|
|
||||||
import { AppModule } from './../src/app.module';
|
|
||||||
|
|
||||||
describe('AppController (e2e)', () => {
|
|
||||||
let app: INestApplication;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AppModule],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
|
||||||
await app.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('/ (GET)', () => {
|
|
||||||
return request(app.getHttpServer())
|
|
||||||
.get('/')
|
|
||||||
.expect(200)
|
|
||||||
.expect('Hello World!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
37
server/test/test-utils.ts
Normal file
37
server/test/test-utils.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { getConnection } from 'typeorm';
|
||||||
|
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { TestingModuleBuilder } from '@nestjs/testing';
|
||||||
|
import { AuthUserDto } from '../src/decorators/auth-user.decorator';
|
||||||
|
import { JwtAuthGuard } from '../src/modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
type CustomAuthCallback = () => AuthUserDto;
|
||||||
|
|
||||||
|
export async function clearDb() {
|
||||||
|
const entities = getConnection().entityMetadatas;
|
||||||
|
for (const entity of entities) {
|
||||||
|
const repository = getConnection().getRepository(entity.name);
|
||||||
|
await repository.query(`TRUNCATE ${entity.tableName} RESTART IDENTITY CASCADE;`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthUser(): AuthUserDto {
|
||||||
|
return {
|
||||||
|
id: '3108ac14-8afb-4b7e-87fd-39ebb6b79750',
|
||||||
|
email: 'test@email.com',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auth(builder: TestingModuleBuilder): TestingModuleBuilder {
|
||||||
|
return authCustom(builder, getAuthUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCallback): TestingModuleBuilder {
|
||||||
|
const canActivate: CanActivate = {
|
||||||
|
canActivate: (context: ExecutionContext) => {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
req.user = callback();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return builder.overrideGuard(JwtAuthGuard).useValue(canActivate);
|
||||||
|
}
|
||||||
96
server/test/user.e2e-spec.ts
Normal file
96
server/test/user.e2e-spec.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { clearDb, authCustom } from './test-utils';
|
||||||
|
import { databaseConfig } from '../src/config/database.config';
|
||||||
|
import { UserModule } from '../src/api-v1/user/user.module';
|
||||||
|
import { AuthModule } from '../src/api-v1/auth/auth.module';
|
||||||
|
import { AuthService } from '../src/api-v1/auth/auth.service';
|
||||||
|
import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module';
|
||||||
|
import { SignUpDto } from '../src/api-v1/auth/dto/sign-up.dto';
|
||||||
|
import { AuthUserDto } from '../src/decorators/auth-user.decorator';
|
||||||
|
|
||||||
|
function _createUser(authService: AuthService, data: SignUpDto) {
|
||||||
|
return authService.signUp(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('User', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearDb();
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('without auth', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [UserModule, ImmichJwtModule, TypeOrmModule.forRoot(databaseConfig)],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents fetching users if not auth', async () => {
|
||||||
|
const { status } = await request(app.getHttpServer()).get('/user');
|
||||||
|
expect(status).toEqual(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with auth', () => {
|
||||||
|
let authService: AuthService;
|
||||||
|
let authUser: AuthUserDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const builder = Test.createTestingModule({
|
||||||
|
imports: [UserModule, AuthModule, TypeOrmModule.forRoot(databaseConfig)],
|
||||||
|
});
|
||||||
|
const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
authService = app.get(AuthService);
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with users in DB', () => {
|
||||||
|
const authUserEmail = 'auth-user@test.com';
|
||||||
|
const userOneEmail = 'one@test.com';
|
||||||
|
const userTwoEmail = 'two@test.com';
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await Promise.allSettled([
|
||||||
|
_createUser(authService, { email: authUserEmail, password: '1234' }).then((user) => (authUser = user)),
|
||||||
|
_createUser(authService, { email: userOneEmail, password: '1234' }),
|
||||||
|
_createUser(authService, { email: userTwoEmail, password: '1234' }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches the user collection excluding the auth user', async () => {
|
||||||
|
const { status, body } = await request(app.getHttpServer()).get('/user');
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
expect(body).toHaveLength(2);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
email: userOneEmail,
|
||||||
|
id: expect.anything(),
|
||||||
|
createdAt: expect.anything(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: userTwoEmail,
|
||||||
|
id: expect.anything(),
|
||||||
|
createdAt: expect.anything(),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(body).toEqual(expect.not.arrayContaining([expect.objectContaining({ email: authUserEmail })]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,15 +9,13 @@
|
|||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
"upload"
|
"upload"
|
||||||
],
|
],
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
4
web/.dockerignore
Normal file
4
web/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
upload/
|
||||||
|
dist/
|
||||||
|
|
||||||
20
web/.eslintrc.cjs
Normal file
20
web/.eslintrc.cjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||||
|
plugins: ['svelte3', '@typescript-eslint'],
|
||||||
|
ignorePatterns: ['*.cjs'],
|
||||||
|
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||||
|
settings: {
|
||||||
|
'svelte3/typescript': () => require('typescript')
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
}
|
||||||
|
};
|
||||||
10
web/.gitignore
vendored
Normal file
10
web/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.vercel
|
||||||
|
.output
|
||||||
1
web/.npmrc
Normal file
1
web/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
7
web/.prettierrc
Normal file
7
web/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
8
web/CHANGELOG.md
Normal file
8
web/CHANGELOG.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# default-template
|
||||||
|
|
||||||
|
## 0.0.2-next.0
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- [chore] upgrade cookie library ([#4592](https://github.com/sveltejs/kit/pull/4592))
|
||||||
39
web/Dockerfile
Normal file
39
web/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Our Node base image
|
||||||
|
FROM node:16-alpine3.14 as base
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
RUN chown node:node /usr/src/app
|
||||||
|
|
||||||
|
COPY --chown=node:node package*.json ./
|
||||||
|
|
||||||
|
RUN apk add --update-cache build-base python3
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY --chown=node:node . .
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
EXPOSE 24678
|
||||||
|
|
||||||
|
FROM base AS dev
|
||||||
|
ENV CHOKIDAR_USEPOLLING=true
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
|
|
||||||
|
FROM node:16-alpine3.14 as prod
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
RUN chown node:node /usr/src/app
|
||||||
|
|
||||||
|
COPY --chown=node:node package*.json ./
|
||||||
|
COPY --chown=node:node . .
|
||||||
|
|
||||||
|
RUN apk add --update-cache build-base python3
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|
||||||
|
# Issue build command in entrypoint.sh to capture user .env file instead of the builder .env file.
|
||||||
38
web/README.md
Normal file
38
web/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# create-svelte
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm init svelte
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm init svelte my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||||
1
web/entrypoint.sh
Normal file
1
web/entrypoint.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npm run build && node /usr/src/app/build/index.js
|
||||||
5184
web/package-lock.json
generated
Normal file
5184
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
web/package.json
Normal file
47
web/package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "svelte-kit dev --host 0.0.0.0 --port 3002",
|
||||||
|
"build": "svelte-kit build",
|
||||||
|
"package": "svelte-kit package",
|
||||||
|
"preview": "svelte-kit preview",
|
||||||
|
"prepare": "svelte-kit sync",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||||
|
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "next",
|
||||||
|
"@sveltejs/adapter-node": "^1.0.0-next.73",
|
||||||
|
"@sveltejs/kit": "next",
|
||||||
|
"@types/bcrypt": "^5.0.0",
|
||||||
|
"@types/cookie": "^0.4.1",
|
||||||
|
"@types/lodash": "^4.14.182",
|
||||||
|
"@types/lodash-es": "^4.17.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||||
|
"@typescript-eslint/parser": "^5.10.1",
|
||||||
|
"autoprefixer": "^10.4.7",
|
||||||
|
"eslint": "^8.12.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"postcss": "^8.4.13",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"prettier-plugin-svelte": "^2.5.0",
|
||||||
|
"svelte": "^3.46.0",
|
||||||
|
"svelte-check": "^2.2.6",
|
||||||
|
"svelte-preprocess": "^4.10.1",
|
||||||
|
"tailwindcss": "^3.0.24",
|
||||||
|
"tslib": "^2.3.1",
|
||||||
|
"typescript": "~4.6.2"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^0.4.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"moment": "^2.29.3",
|
||||||
|
"svelte-material-icons": "^2.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
web/postcss.config.cjs
Normal file
6
web/postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
31
web/src/app.css
Normal file
31
web/src/app.css
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@300;400;500;600;700&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Snowburst+One&display=swap');
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: 'Work Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f6f8fe;
|
||||||
|
color: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.immich-form-input {
|
||||||
|
@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm
|
||||||
|
}
|
||||||
|
|
||||||
|
.immich-form-label {
|
||||||
|
@apply font-medium text-sm text-gray-500
|
||||||
|
}
|
||||||
|
|
||||||
|
.immich-btn-primary {
|
||||||
|
@apply bg-immich-primary text-gray-100 border rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary hover:shadow-lg text-sm font-medium
|
||||||
|
}
|
||||||
|
}
|
||||||
32
web/src/app.d.ts
vendored
Normal file
32
web/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/// <reference types="@sveltejs/kit" />
|
||||||
|
|
||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare namespace App {
|
||||||
|
interface Locals {
|
||||||
|
user?: {
|
||||||
|
id: string,
|
||||||
|
email: string,
|
||||||
|
accessToken: string,
|
||||||
|
firstName: string,
|
||||||
|
lastName: string,
|
||||||
|
isAdmin: boolean,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface Platform {}
|
||||||
|
|
||||||
|
interface Session {
|
||||||
|
user?: {
|
||||||
|
id: string,
|
||||||
|
email: string,
|
||||||
|
accessToken: string,
|
||||||
|
firstName: string,
|
||||||
|
lastName: string
|
||||||
|
isAdmin: boolean,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface Stuff {}
|
||||||
|
}
|
||||||
|
|
||||||
12
web/src/app.html
Normal file
12
web/src/app.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%svelte.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%svelte.head%
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>%svelte.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
60
web/src/hooks.ts
Normal file
60
web/src/hooks.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { ExternalFetch, GetSession, Handle } from '@sveltejs/kit';
|
||||||
|
import * as cookie from 'cookie';
|
||||||
|
import { serverEndpoint } from '$lib/constants';
|
||||||
|
import { session } from '$app/stores';
|
||||||
|
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve, }) => {
|
||||||
|
const cookies = cookie.parse(event.request.headers.get('cookie') || '');
|
||||||
|
|
||||||
|
if (!cookies.session) {
|
||||||
|
return await resolve(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { email, isAdmin, firstName, lastName, id, accessToken } = JSON.parse(cookies.session);
|
||||||
|
|
||||||
|
const res = await fetch(`${serverEndpoint}/auth/validateToken`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status === 201) {
|
||||||
|
event.locals.user = {
|
||||||
|
id,
|
||||||
|
accessToken,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
isAdmin,
|
||||||
|
email
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await resolve(event);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error parsing session', error);
|
||||||
|
return await resolve(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSession: GetSession = async ({ locals }) => {
|
||||||
|
|
||||||
|
if (!locals.user) return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: locals.user.id,
|
||||||
|
accessToken: locals.user.accessToken,
|
||||||
|
firstName: locals.user.firstName,
|
||||||
|
lastName: locals.user.lastName,
|
||||||
|
isAdmin: locals.user.isAdmin,
|
||||||
|
email: locals.user.email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
60
web/src/lib/api.ts
Normal file
60
web/src/lib/api.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { serverEndpoint } from './constants';
|
||||||
|
|
||||||
|
type ISend = {
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
data?: any,
|
||||||
|
token: string
|
||||||
|
customHeaders?: Record<string, string>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type IOption = {
|
||||||
|
method: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
body: any
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send({ method, path, data, token, customHeaders }: ISend) {
|
||||||
|
const opts: IOption = { method, headers: {} } as IOption;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
opts.headers['Content-Type'] = 'application/json';
|
||||||
|
opts.body = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customHeaders) {
|
||||||
|
console.log(customHeaders);
|
||||||
|
// opts.headers[customHeader.$1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
opts.headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(`${serverEndpoint}/${path}`, opts)
|
||||||
|
.then((r) => r.text())
|
||||||
|
.then((json) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch (err) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRequest(path: string, token: string, customHeaders?: Record<string, string>) {
|
||||||
|
return send({ method: 'GET', path, token, customHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delRequest(path: string, token: string, customHeaders?: Record<string, string>) {
|
||||||
|
return send({ method: 'DELETE', path, token, customHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postRequest(path: string, data: any, token: string, customHeaders?: Record<string, string>) {
|
||||||
|
return send({ method: 'POST', path, data, token, customHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putRequest(path: string, data: any, token: string, customHeaders?: Record<string, string>) {
|
||||||
|
return send({ method: 'PUT', path, data, token, customHeaders });
|
||||||
|
}
|
||||||
74
web/src/lib/auth-api.ts
Normal file
74
web/src/lib/auth-api.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
type AdminRegistrationResult = Promise<{
|
||||||
|
error?: string
|
||||||
|
success?: string
|
||||||
|
user?: {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type LoginResult = Promise<{
|
||||||
|
error?: string
|
||||||
|
success?: string
|
||||||
|
needUpdate?: boolean
|
||||||
|
needSelectAdmin?: boolean
|
||||||
|
user?: {
|
||||||
|
accessToken: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
isAdmin: boolean
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
|
||||||
|
type UpdateResult = Promise<{
|
||||||
|
error?: string
|
||||||
|
success?: string,
|
||||||
|
user?: {
|
||||||
|
accessToken: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
isAdmin: boolean
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
export async function sendRegistrationForm(form: HTMLFormElement): AdminRegistrationResult {
|
||||||
|
|
||||||
|
const response = await fetch(form.action, {
|
||||||
|
method: form.method,
|
||||||
|
body: new FormData(form),
|
||||||
|
headers: { accept: 'application/json' },
|
||||||
|
})
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function sendLoginForm(form: HTMLFormElement): LoginResult {
|
||||||
|
|
||||||
|
const response = await fetch(form.action, {
|
||||||
|
method: form.method,
|
||||||
|
body: new FormData(form),
|
||||||
|
headers: { accept: 'application/json' },
|
||||||
|
})
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendUpdateForm(form: HTMLFormElement): UpdateResult {
|
||||||
|
|
||||||
|
const response = await fetch(form.action, {
|
||||||
|
method: form.method,
|
||||||
|
body: new FormData(form),
|
||||||
|
headers: { accept: 'application/json' },
|
||||||
|
})
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user