Compare commits
64 Commits
v1.27.0_37
...
v1.29.2_43
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e997bd371b | ||
|
|
400167f4ef | ||
|
|
572f6d833d | ||
|
|
2e06be5155 | ||
|
|
62121470a8 | ||
|
|
e3ccc3ee6b | ||
|
|
ece94f6bdc | ||
|
|
03fc0703c0 | ||
|
|
0d13b25f56 | ||
|
|
75c2067836 | ||
|
|
824da6a07b | ||
|
|
2c2ea24dc4 | ||
|
|
47b73a5b64 | ||
|
|
6b3f8e548d | ||
|
|
0ea483f901 | ||
|
|
97aed8ef23 | ||
|
|
0ee3fe9157 | ||
|
|
434770155f | ||
|
|
7e8bf94543 | ||
|
|
8d8944705c | ||
|
|
7c9c1a5169 | ||
|
|
1a6c16d8ea | ||
|
|
ccf792f9d3 | ||
|
|
789bc8563c | ||
|
|
99a50f70dd | ||
|
|
9bef411056 | ||
|
|
e79e92c60f | ||
|
|
858ad43d3b | ||
|
|
5761765ea7 | ||
|
|
6abc733763 | ||
|
|
4271e24e59 | ||
|
|
9e4ed2214b | ||
|
|
011332e509 | ||
|
|
5403ef4d84 | ||
|
|
31739aca02 | ||
|
|
8f2e7b6f65 | ||
|
|
4ed647c43d | ||
|
|
f88ff4fb5c | ||
|
|
cc4881d633 | ||
|
|
d856b35afc | ||
|
|
b6d025da09 | ||
|
|
cc79ff1ca3 | ||
|
|
131aa2b6be | ||
|
|
02a6b73122 | ||
|
|
d87366c095 | ||
|
|
4f7a3afbfc | ||
|
|
6725954b70 | ||
|
|
4fe535e5e8 | ||
|
|
aed94bfc4c | ||
|
|
de996c0a81 | ||
|
|
1a39aa4da5 | ||
|
|
1f4ba73da7 | ||
|
|
836b174d33 | ||
|
|
853a65aef1 | ||
|
|
566039b93f | ||
|
|
18a7ff8726 | ||
|
|
6ffdf167fe | ||
|
|
6b702b13e4 | ||
|
|
f476bd985b | ||
|
|
92c4f0598b | ||
|
|
a337402124 | ||
|
|
209e6332b3 | ||
|
|
645bd8a109 | ||
|
|
9a471d80f7 |
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
@@ -13,18 +13,29 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Run Immich Server 2E2 Test
|
- name: Run Immich Server 2E2 Test
|
||||||
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
|
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
|
||||||
|
|
||||||
unit-tests:
|
server-unit-tests:
|
||||||
name: Run unit test suites
|
name: Run server unit test suites and checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cd server && npm install && npm run test
|
run: cd server && npm ci && npm run check:all
|
||||||
|
|
||||||
|
web-unit-tests:
|
||||||
|
name: Run web unit test suites and checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cd web && npm ci && npm run check:all
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -27,6 +27,7 @@
|
|||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
|
- [Update](#update)
|
||||||
- [Mobile App](#-mobile-app)
|
- [Mobile App](#-mobile-app)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Support](#support)
|
- [Support](#support)
|
||||||
@@ -97,6 +98,8 @@ There are several services that compose Immich:
|
|||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
|
NOTE: When using a reverse proxy in front of Immich (such as NGINX), the reverse proxy might require extra configuration to allow large files to be uploaded (such as client_max_body_size in the case of NGINX).
|
||||||
|
|
||||||
## Testing One-step installation (not recommended for production)
|
## Testing One-step installation (not recommended for production)
|
||||||
|
|
||||||
> ⚠️ *This installation method is for evaluating Immich before futher customization to meet the users' needs.*
|
> ⚠️ *This installation method is for evaluating Immich before futher customization to meet the users' needs.*
|
||||||
@@ -144,6 +147,7 @@ wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.en
|
|||||||
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
|
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
|
||||||
* Populate a secret value for `JWT_SECRET`, you can use this command: `openssl rand -base64 128`
|
* Populate a secret value for `JWT_SECRET`, you can use this command: `openssl rand -base64 128`
|
||||||
* [Optional] Populate Mapbox value to use reverse geocoding.
|
* [Optional] Populate Mapbox value to use reverse geocoding.
|
||||||
|
* [Optional] Populate `TZ` as your timezone, default is `Etc/UTC`.
|
||||||
|
|
||||||
### Step 3 - Start the containers
|
### Step 3 - Start the containers
|
||||||
|
|
||||||
@@ -170,13 +174,21 @@ wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.en
|
|||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
## Update
|
||||||
|
|
||||||
|
If you have installed, you can update the application by navigate to the directory that contains the `docker-compose.yml` file and run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose pull && docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
# Mobile app
|
# Mobile app
|
||||||
|
|
||||||
| F-Droid | Google Play | iOS |
|
| F-Droid | Google Play | iOS |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| <a href="https://f-droid.org/packages/app.alextran.immich"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> | <p align="left"> <img src="design/google-play-qr-code.png" width="200" title="Google Play Store"> <p/> | <p align="left"> <img src="design/ios-qr-code.png" width="200" title="Apple App Store"> <p/> |
|
| <a href="https://f-droid.org/packages/app.alextran.immich"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> | <p align="left"> <a href="https://play.google.com/store/apps/details?id=app.alextran.immich"><img src="design/google-play-qr-code.png" width="200" title="Google Play Store"></a> <p/> | <p align="left"> <a href="https://apps.apple.com/us/app/immich/id1613945652"><img src="design/ios-qr-code.png" width="200" title="Apple App Store"></a> <p/> |
|
||||||
|
|
||||||
> *The App version might be lagging behind the latest release due to the review process.*
|
> *The Play/App Store version might be lagging behind the latest release due to the review process.*
|
||||||
|
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
@@ -225,7 +237,7 @@ Cheers! 🎉
|
|||||||
|
|
||||||
## TensorFlow Build Issue
|
## TensorFlow Build Issue
|
||||||
|
|
||||||
*This is a known issue for incorrect Promox setup*
|
*This is a known issue for incorrect Proxmox setup*
|
||||||
|
|
||||||
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`:
|
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`:
|
||||||
|
|
||||||
@@ -233,7 +245,7 @@ TensorFlow doesn't run with older CPU architecture, it requires a CPU with AVX a
|
|||||||
more /proc/cpuinfo | grep flags
|
more /proc/cpuinfo | grep flags
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are running virtualization in Promox, the VM doesn't have the flag enabled.
|
If you are running virtualization in Proxmox, 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.
|
||||||
|
|
||||||
|
|||||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security issues to `alex.tran1502@gmail.com`
|
||||||
32
dev-setup.md
Normal file
32
dev-setup.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Development Setup
|
||||||
|
|
||||||
|
## Lint / format extensions
|
||||||
|
|
||||||
|
Setting these in the IDE give a better developer experience auto-formatting code on save and providing instant feedback on lint issues.
|
||||||
|
|
||||||
|
### VSCode
|
||||||
|
Install Prettier, ESLint and Svelte extensions.
|
||||||
|
|
||||||
|
in User `settings.json` (`cmd + shift + p` and search for Open User Settings JSON) add the following:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"[javascript][typescript][css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"svelte.enable-ts-plugin": true,
|
||||||
|
"eslint.validate": ["javascript", "svelte"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running tests / checks
|
||||||
|
|
||||||
|
In both server and web:
|
||||||
|
`npm run check:all`
|
||||||
@@ -36,6 +36,11 @@ REDIS_HOSTNAME=immich_redis
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# Log message level - [simple|verbose]
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
LOG_LEVEL=simple
|
||||||
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
@@ -64,3 +69,11 @@ MAPBOX_KEY=
|
|||||||
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
||||||
|
|
||||||
PUBLIC_LOGIN_PAGE_MESSAGE=
|
PUBLIC_LOGIN_PAGE_MESSAGE=
|
||||||
|
|
||||||
|
# For correctly display your local time zone on the web, you can set the time zone here.
|
||||||
|
# Should work fine by default value, however, in case of incorrect timezone in EXIF, this value
|
||||||
|
# should be set to the correct timezone.
|
||||||
|
# Command to get timezone:
|
||||||
|
# - Linux: curl -s http://ip-api.com/json/ | grep -oP '(?<=timezone":")(.*?)(?=")'
|
||||||
|
|
||||||
|
# TZ=Etc/UTC
|
||||||
@@ -102,8 +102,7 @@ services:
|
|||||||
context: ../nginx
|
context: ../nginx
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- 2283:80
|
- 2283:8080
|
||||||
- 2284:443
|
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ services:
|
|||||||
container_name: immich_proxy
|
container_name: immich_proxy
|
||||||
image: altran1502/immich-proxy:staging
|
image: altran1502/immich-proxy:staging
|
||||||
ports:
|
ports:
|
||||||
- 2283:80
|
- 2283:8080
|
||||||
- 2284:443
|
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ services:
|
|||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
- PUBLIC_TZ=${TZ}
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
@@ -72,7 +74,7 @@ services:
|
|||||||
container_name: immich_proxy
|
container_name: immich_proxy
|
||||||
image: altran1502/immich-proxy:release
|
image: altran1502/immich-proxy:release
|
||||||
ports:
|
ports:
|
||||||
- 2283:80
|
- 2283:8080
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
36
install.sh
36
install.sh
@@ -2,12 +2,17 @@ echo "Starting Immich installation..."
|
|||||||
|
|
||||||
ip_address=$(hostname -I | awk '{print $1}')
|
ip_address=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
release_version=$(curl --silent "https://api.github.com/repos/immich-app/immich/releases/latest" |
|
||||||
|
grep '"tag_name":' |
|
||||||
|
sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\032[0;31m'
|
GREEN='\032[0;31m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
machine_has() {
|
get_release_version() {
|
||||||
type "$1" >/dev/null 2>&1
|
curl --silent "https://api.github.com/repos/immich-app/immich/releases/latest" | # Get latest release from GitHub api
|
||||||
|
grep '"tag_name":' | # Get tag line
|
||||||
|
sed -E 's/.*"([^"]+)".*/\1/' # Pluck JSON value
|
||||||
}
|
}
|
||||||
|
|
||||||
create_immich_directory() {
|
create_immich_directory() {
|
||||||
@@ -17,12 +22,12 @@ create_immich_directory() {
|
|||||||
|
|
||||||
download_docker_compose_file() {
|
download_docker_compose_file() {
|
||||||
echo "Downloading docker-compose.yml..."
|
echo "Downloading docker-compose.yml..."
|
||||||
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
|
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
download_dot_env_file() {
|
download_dot_env_file() {
|
||||||
echo "Downloading .env file..."
|
echo "Downloading .env file..."
|
||||||
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
|
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
populate_upload_location() {
|
populate_upload_location() {
|
||||||
@@ -45,18 +50,21 @@ populate_upload_location() {
|
|||||||
start_docker_compose() {
|
start_docker_compose() {
|
||||||
echo "Starting Immich's docker containers"
|
echo "Starting Immich's docker containers"
|
||||||
|
|
||||||
if machine_has "docker compose"; then {
|
if docker compose &>/dev/null; then
|
||||||
docker compose up --remove-orphans -d
|
docker_bin="docker compose"
|
||||||
|
elif docker-compose &>/dev/null; then
|
||||||
show_friendly_message
|
docker_bin="docker-compose"
|
||||||
exit 0
|
else
|
||||||
}; fi
|
echo 'Cannot find `docker compose` or `docker-compose`.'
|
||||||
|
exit 1
|
||||||
if machine_has "docker-compose"; then
|
fi
|
||||||
docker-compose up --remove-orphans -d
|
|
||||||
|
|
||||||
|
if $docker_bin up --remove-orphans -d; then
|
||||||
show_friendly_message
|
show_friendly_message
|
||||||
exit 0
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Could not start. Check for errors above."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +73,7 @@ show_friendly_message() {
|
|||||||
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
|
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
|
||||||
echo "The backup (or upload) location is $upload_location"
|
echo "The backup (or upload) location is $upload_location"
|
||||||
echo "---------------------------------------------------"
|
echo "---------------------------------------------------"
|
||||||
echo "If you want to confgure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
|
echo "If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
|
||||||
|
|
||||||
1. First bring down the containers with the command 'docker-compose down' in the immich-app directory,
|
1. First bring down the containers with the command 'docker-compose down' in the immich-app directory,
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "app.alextran.immich"
|
applicationId "app.alextran.immich"
|
||||||
minSdkVersion 21
|
minSdkVersion 23
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
<service android:name=".AppClearedService" android:stopWithTask="false" />
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package app.alextran.immich
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catches the event when either the system or the user kills the app
|
||||||
|
* (does not apply on force close!)
|
||||||
|
*/
|
||||||
|
class AppClearedService() : Service() {
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent) {
|
||||||
|
ContentObserverWorker.workManagerAppClearedWorkaround(applicationContext)
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.content.Intent
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
@@ -44,30 +39,30 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val ctx = context!!
|
val ctx = context!!
|
||||||
when(call.method) {
|
when(call.method) {
|
||||||
"initialize" -> { // needs to be called prior to any other method
|
"enable" -> {
|
||||||
val args = call.arguments<ArrayList<*>>()!!
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
.edit().putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long).apply()
|
.edit()
|
||||||
|
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
|
||||||
|
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
|
||||||
|
.apply()
|
||||||
|
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
"start" -> {
|
"configure" -> {
|
||||||
val args = call.arguments<ArrayList<*>>()!!
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
val immediate = args.get(0) as Boolean
|
val requireUnmeteredNetwork = args.get(0) as Boolean
|
||||||
val keepExisting = args.get(1) as Boolean
|
val requireCharging = args.get(1) as Boolean
|
||||||
val requireUnmeteredNetwork = args.get(2) as Boolean
|
ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging)
|
||||||
val requireCharging = args.get(3) as Boolean
|
|
||||||
val notificationTitle = args.get(4) as String
|
|
||||||
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
|
||||||
.edit().putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, notificationTitle).apply()
|
|
||||||
BackupWorker.startWork(ctx, immediate, keepExisting, requireUnmeteredNetwork, requireCharging)
|
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
"stop" -> {
|
"disable" -> {
|
||||||
|
ContentObserverWorker.disable(ctx)
|
||||||
BackupWorker.stopWork(ctx)
|
BackupWorker.stopWork(ctx)
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
"isEnabled" -> {
|
"isEnabled" -> {
|
||||||
result.success(BackupWorker.isEnabled(ctx))
|
result.success(ContentObserverWorker.isEnabled(ctx))
|
||||||
}
|
}
|
||||||
"isIgnoringBatteryOptimizations" -> {
|
"isIgnoringBatteryOptimizations" -> {
|
||||||
result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
|
result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
|
||||||
|
|||||||
@@ -8,17 +8,12 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.BaseColumns
|
|
||||||
import android.provider.MediaStore.MediaColumns
|
|
||||||
import android.provider.MediaStore.Images.Media
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.concurrent.futures.ResolvableFuture
|
import androidx.concurrent.futures.ResolvableFuture
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.NetworkType
|
import androidx.work.NetworkType
|
||||||
@@ -26,6 +21,7 @@ import androidx.work.WorkerParameters
|
|||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.embedding.engine.dart.DartExecutor
|
import io.flutter.embedding.engine.dart.DartExecutor
|
||||||
@@ -41,14 +37,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
* Starts the Dart runtime/engine and calls `_nativeEntry` function in
|
* Starts the Dart runtime/engine and calls `_nativeEntry` function in
|
||||||
* `background.service.dart` to run the actual backup logic.
|
* `background.service.dart` to run the actual backup logic.
|
||||||
* Called by Android WorkManager when all constraints for the work are met,
|
* Called by Android WorkManager when all constraints for the work are met,
|
||||||
* i.e. a new photo/video is created on the device AND battery is not low.
|
* i.e. battery is not low and optionally Wifi and charging are active.
|
||||||
* Optionally, unmetered network (wifi) and charging can be required.
|
|
||||||
* As this work is not triggered periodically, but on content change, the
|
|
||||||
* worker enqueues itself again with the same settings.
|
|
||||||
* In case the worker is stopped by the system (e.g. constraints like wifi
|
|
||||||
* are no longer met, or the system needs memory resources for more other
|
|
||||||
* more important work), the worker is replaced without the constraint on
|
|
||||||
* changed contents to run again as soon as deemed possible by the system.
|
|
||||||
*/
|
*/
|
||||||
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler {
|
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
@@ -57,14 +46,13 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
private lateinit var backgroundChannel: MethodChannel
|
private lateinit var backgroundChannel: MethodChannel
|
||||||
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
|
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
|
||||||
|
private var timeBackupStarted: Long = 0L
|
||||||
|
|
||||||
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
||||||
|
|
||||||
|
Log.d(TAG, "startWork")
|
||||||
|
|
||||||
val ctx = applicationContext
|
val ctx = applicationContext
|
||||||
// enqueue itself once again to continue to listen on added photos/videos
|
|
||||||
enqueueMoreWork(ctx,
|
|
||||||
requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
|
|
||||||
requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false))
|
|
||||||
|
|
||||||
if (!flutterLoader.initialized()) {
|
if (!flutterLoader.initialized()) {
|
||||||
flutterLoader.startInitialization(ctx)
|
flutterLoader.startInitialization(ctx)
|
||||||
@@ -73,14 +61,16 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
// Create a Notification channel if necessary
|
// Create a Notification channel if necessary
|
||||||
createChannel()
|
createChannel()
|
||||||
}
|
}
|
||||||
|
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
|
||||||
if (isIgnoringBatteryOptimizations) {
|
if (isIgnoringBatteryOptimizations) {
|
||||||
// normal background services can only up to 10 minutes
|
// normal background services can only up to 10 minutes
|
||||||
// foreground services are allowed to run indefinitely
|
// foreground services are allowed to run indefinitely
|
||||||
// requires battery optimizations to be disabled (either manually by the user
|
// requires battery optimizations to be disabled (either manually by the user
|
||||||
// or by the system learning that immich is important to the user)
|
// or by the system learning that immich is important to the user)
|
||||||
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
|
||||||
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
|
|
||||||
setForegroundAsync(createForegroundInfo(title))
|
setForegroundAsync(createForegroundInfo(title))
|
||||||
|
} else {
|
||||||
|
showBackgroundInfo(title)
|
||||||
}
|
}
|
||||||
engine = FlutterEngine(ctx)
|
engine = FlutterEngine(ctx)
|
||||||
|
|
||||||
@@ -115,6 +105,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopped() {
|
override fun onStopped() {
|
||||||
|
Log.d(TAG, "onStopped")
|
||||||
// called when the system has to stop this worker because constraints are
|
// called when the system has to stop this worker because constraints are
|
||||||
// no longer met or the system needs resources for more important tasks
|
// no longer met or the system needs resources for more important tasks
|
||||||
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
||||||
@@ -130,24 +121,18 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
|
|
||||||
private fun stopEngine(result: Result?) {
|
private fun stopEngine(result: Result?) {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
Log.d(TAG, "stopEngine result=${result}")
|
||||||
resolvableFuture.set(result)
|
resolvableFuture.set(result)
|
||||||
} else if (engine != null && inputData.getInt(DATA_KEY_RETRIES, 0) == 0) {
|
|
||||||
// stopped by system and this is the first time (content change constraints active)
|
|
||||||
// replace the task without the content constraints to finish the backup as soon as possible
|
|
||||||
enqueueMoreWork(applicationContext,
|
|
||||||
immediate = true,
|
|
||||||
requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
|
|
||||||
requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
|
|
||||||
initialDelayInMs = ONE_MINUTE,
|
|
||||||
retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
|
|
||||||
}
|
}
|
||||||
engine?.destroy()
|
engine?.destroy()
|
||||||
engine = null
|
engine = null
|
||||||
|
clearBackgroundNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"initialized" ->
|
"initialized" -> {
|
||||||
|
timeBackupStarted = SystemClock.uptimeMillis()
|
||||||
backgroundChannel.invokeMethod(
|
backgroundChannel.invokeMethod(
|
||||||
"onAssetsChanged",
|
"onAssetsChanged",
|
||||||
null,
|
null,
|
||||||
@@ -163,25 +148,18 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
override fun success(receivedResult: Any?) {
|
override fun success(receivedResult: Any?) {
|
||||||
val success = receivedResult as Boolean
|
val success = receivedResult as Boolean
|
||||||
stopEngine(if(success) Result.success() else Result.retry())
|
stopEngine(if(success) Result.success() else Result.retry())
|
||||||
if (!success && inputData.getInt(DATA_KEY_RETRIES, 0) == 0) {
|
|
||||||
// there was an error (e.g. server not available)
|
|
||||||
// replace the task without the content constraints to finish the backup as soon as possible
|
|
||||||
enqueueMoreWork(applicationContext,
|
|
||||||
immediate = true,
|
|
||||||
requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
|
|
||||||
requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
|
|
||||||
initialDelayInMs = ONE_MINUTE,
|
|
||||||
retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
"updateNotification" -> {
|
"updateNotification" -> {
|
||||||
val args = call.arguments<ArrayList<*>>()!!
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
val title = args.get(0) as String
|
val title = args.get(0) as String
|
||||||
val content = args.get(1) as String
|
val content = args.get(1) as String
|
||||||
if (isIgnoringBatteryOptimizations) {
|
if (isIgnoringBatteryOptimizations) {
|
||||||
setForegroundAsync(createForegroundInfo(title, content))
|
setForegroundAsync(createForegroundInfo(title, content))
|
||||||
|
} else {
|
||||||
|
showBackgroundInfo(title, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"showError" -> {
|
"showError" -> {
|
||||||
@@ -192,6 +170,14 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
showError(title, content, individualTag)
|
showError(title, content, individualTag)
|
||||||
}
|
}
|
||||||
"clearErrorNotifications" -> clearErrorNotifications()
|
"clearErrorNotifications" -> clearErrorNotifications()
|
||||||
|
"hasContentChanged" -> {
|
||||||
|
val lastChange = applicationContext
|
||||||
|
.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted)
|
||||||
|
val hasContentChanged = lastChange > timeBackupStarted;
|
||||||
|
timeBackupStarted = SystemClock.uptimeMillis()
|
||||||
|
r.success(hasContentChanged)
|
||||||
|
}
|
||||||
else -> r.notImplemented()
|
else -> r.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +197,22 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
notificationManager.cancel(NOTIFICATION_ERROR_ID)
|
notificationManager.cancel(NOTIFICATION_ERROR_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showBackgroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null) {
|
||||||
|
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setTicker(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearBackgroundNotification() {
|
||||||
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
|
||||||
private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
|
private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
|
||||||
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
@@ -233,89 +235,61 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
companion object {
|
companion object {
|
||||||
const val SHARED_PREF_NAME = "immichBackgroundService"
|
const val SHARED_PREF_NAME = "immichBackgroundService"
|
||||||
const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
|
const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
|
||||||
const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled"
|
|
||||||
const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
|
const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
|
||||||
|
const val SHARED_PREF_LAST_CHANGE = "lastChange"
|
||||||
|
|
||||||
private const val TASK_NAME = "immich/photoListener"
|
private const val TASK_NAME_BACKUP = "immich/BackupWorker"
|
||||||
private const val DATA_KEY_UNMETERED = "unmetered"
|
|
||||||
private const val DATA_KEY_CHARGING = "charging"
|
|
||||||
private const val DATA_KEY_RETRIES = "retries"
|
|
||||||
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
|
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
|
||||||
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
|
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
|
||||||
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
|
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
|
||||||
private const val NOTIFICATION_ID = 1
|
private const val NOTIFICATION_ID = 1
|
||||||
private const val NOTIFICATION_ERROR_ID = 2
|
private const val NOTIFICATION_ERROR_ID = 2
|
||||||
private const val ONE_MINUTE: Long = 60000
|
private const val ONE_MINUTE = 60000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueues the `BackupWorker` to run when all constraints are met.
|
* Enqueues the BackupWorker to run once the constraints are met
|
||||||
*
|
|
||||||
* @param context Android Context
|
|
||||||
* @param immediate whether to enqueue(replace) the worker without the content change constraint
|
|
||||||
* @param keepExisting if true, use `ExistingWorkPolicy.KEEP`, else `ExistingWorkPolicy.APPEND_OR_REPLACE`
|
|
||||||
* @param requireUnmeteredNetwork if true, task only runs if connected to wifi
|
|
||||||
* @param requireCharging if true, task only runs if device is charging
|
|
||||||
* @param retries retry count (should be 0 unless an error occured and this is a retry)
|
|
||||||
*/
|
*/
|
||||||
fun startWork(context: Context,
|
fun enqueueBackupWorker(context: Context,
|
||||||
immediate: Boolean = false,
|
requireWifi: Boolean = false,
|
||||||
keepExisting: Boolean = false,
|
requireCharging: Boolean = false,
|
||||||
requireUnmeteredNetwork: Boolean = false,
|
delayMilliseconds: Long = 0L) {
|
||||||
requireCharging: Boolean = false) {
|
val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds)
|
||||||
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest)
|
||||||
.edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, true).apply()
|
Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued")
|
||||||
enqueueMoreWork(context, immediate, keepExisting, requireUnmeteredNetwork, requireCharging)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enqueueMoreWork(context: Context,
|
/**
|
||||||
immediate: Boolean = false,
|
* Updates the constraints of an already enqueued BackupWorker
|
||||||
keepExisting: Boolean = false,
|
*/
|
||||||
requireUnmeteredNetwork: Boolean = false,
|
fun updateBackupWorker(context: Context,
|
||||||
requireCharging: Boolean = false,
|
requireWifi: Boolean = false,
|
||||||
initialDelayInMs: Long = 0,
|
requireCharging: Boolean = false) {
|
||||||
retries: Int = 0) {
|
try {
|
||||||
if (!isEnabled(context)) {
|
val wm = WorkManager.getInstance(context)
|
||||||
return
|
val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP)
|
||||||
|
val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
|
||||||
|
if (workInfoList != null) {
|
||||||
|
for (workInfo in workInfoList) {
|
||||||
|
if (workInfo.getState() == WorkInfo.State.ENQUEUED) {
|
||||||
|
val workRequest = buildWorkRequest(requireWifi, requireCharging)
|
||||||
|
wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
|
||||||
|
Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, "updateBackupWorker failed: ${e}")
|
||||||
}
|
}
|
||||||
val constraints = Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(if (requireUnmeteredNetwork) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
|
||||||
.setRequiresBatteryNotLow(true)
|
|
||||||
.setRequiresCharging(requireCharging);
|
|
||||||
if (!immediate) {
|
|
||||||
constraints
|
|
||||||
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
|
||||||
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
|
||||||
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
|
||||||
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val inputData = Data.Builder()
|
|
||||||
.putBoolean(DATA_KEY_CHARGING, requireCharging)
|
|
||||||
.putBoolean(DATA_KEY_UNMETERED, requireUnmeteredNetwork)
|
|
||||||
.putInt(DATA_KEY_RETRIES, retries)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val photoCheck = OneTimeWorkRequest.Builder(BackupWorker::class.java)
|
|
||||||
.setConstraints(constraints.build())
|
|
||||||
.setInputData(inputData)
|
|
||||||
.setInitialDelay(initialDelayInMs, TimeUnit.MILLISECONDS)
|
|
||||||
.setBackoffCriteria(
|
|
||||||
BackoffPolicy.EXPONENTIAL,
|
|
||||||
ONE_MINUTE,
|
|
||||||
TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
val policy = if (immediate) ExistingWorkPolicy.REPLACE else (if (keepExisting) ExistingWorkPolicy.KEEP else ExistingWorkPolicy.APPEND_OR_REPLACE)
|
|
||||||
val op = WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME, policy, photoCheck)
|
|
||||||
val result = op.getResult().get()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the currently running worker (if any) and removes it from the work queue
|
* Stops the currently running worker (if any) and removes it from the work queue
|
||||||
*/
|
*/
|
||||||
fun stopWork(context: Context) {
|
fun stopWork(context: Context) {
|
||||||
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP)
|
||||||
.edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply()
|
Log.d(TAG, "stopWork: BackupWorker cancelled")
|
||||||
WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,12 +304,21 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun buildWorkRequest(requireWifi: Boolean = false,
|
||||||
* Return true if the user has enabled the background backup service
|
requireCharging: Boolean = false,
|
||||||
*/
|
delayMilliseconds: Long = 0L): OneTimeWorkRequest {
|
||||||
fun isEnabled(ctx: Context): Boolean {
|
val constraints = Constraints.Builder()
|
||||||
return ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
.setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||||
.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)
|
.setRequiresBatteryNotLow(true)
|
||||||
|
.setRequiresCharging(requireCharging)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
|
||||||
|
.setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
return work
|
||||||
}
|
}
|
||||||
|
|
||||||
private val flutterLoader = FlutterLoader()
|
private val flutterLoader = FlutterLoader()
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package app.alextran.immich
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.Operation
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker executed by Android WorkManager observing content changes (new photos/videos)
|
||||||
|
*
|
||||||
|
* Immediately enqueues the BackupWorker when running.
|
||||||
|
* As this work is not triggered periodically, but on content change, the
|
||||||
|
* worker enqueues itself again after each run.
|
||||||
|
*/
|
||||||
|
class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
if (!isEnabled(applicationContext)) {
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
if (getTriggeredContentUris().size > 0) {
|
||||||
|
startBackupWorker(applicationContext, delayMilliseconds = 0)
|
||||||
|
}
|
||||||
|
enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled"
|
||||||
|
const val SHARED_PREF_REQUIRE_WIFI = "requireWifi"
|
||||||
|
const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging"
|
||||||
|
|
||||||
|
private const val TASK_NAME_OBSERVER = "immich/ContentObserver"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues the `ContentObserverWorker`.
|
||||||
|
*
|
||||||
|
* @param context Android Context
|
||||||
|
*/
|
||||||
|
fun enable(context: Context, immediate: Boolean = false) {
|
||||||
|
// migration to remove any old active background task
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork("immich/photoListener")
|
||||||
|
|
||||||
|
enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
|
||||||
|
Log.d(TAG, "enabled ContentObserverWorker")
|
||||||
|
if (immediate) {
|
||||||
|
startBackupWorker(context, delayMilliseconds = 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the `BackupWorker` to run when all constraints are met.
|
||||||
|
*
|
||||||
|
* @param context Android Context
|
||||||
|
* @param requireWifi if true, task only runs if connected to wifi
|
||||||
|
* @param requireCharging if true, task only runs if device is charging
|
||||||
|
*/
|
||||||
|
fun configureWork(context: Context,
|
||||||
|
requireWifi: Boolean = false,
|
||||||
|
requireCharging: Boolean = false) {
|
||||||
|
context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(SHARED_PREF_SERVICE_ENABLED, true)
|
||||||
|
.putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi)
|
||||||
|
.putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging)
|
||||||
|
.apply()
|
||||||
|
BackupWorker.updateBackupWorker(context, requireWifi, requireCharging)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the currently running worker (if any) and removes it from the work queue
|
||||||
|
*/
|
||||||
|
fun disable(context: Context) {
|
||||||
|
context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply()
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER)
|
||||||
|
Log.d(TAG, "disabled ContentObserverWorker")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the user has enabled the background backup service
|
||||||
|
*/
|
||||||
|
fun isEnabled(ctx: Context): Boolean {
|
||||||
|
return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue and replace the worker without the content trigger but with a short delay
|
||||||
|
*/
|
||||||
|
fun workManagerAppClearedWorkaround(context: Context) {
|
||||||
|
val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
|
||||||
|
.setInitialDelay(500, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
WorkManager
|
||||||
|
.getInstance(context)
|
||||||
|
.enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work)
|
||||||
|
.getResult()
|
||||||
|
.get()
|
||||||
|
Log.d(TAG, "workManagerAppClearedWorkaround")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) {
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
||||||
|
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
|
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
||||||
|
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
|
.setTriggerContentUpdateDelay(5000, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startBackupWorker(context: Context, delayMilliseconds: Long) {
|
||||||
|
val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true)
|
||||||
|
val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
|
||||||
|
BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)
|
||||||
|
sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val TAG = "ContentObserverWorker"
|
||||||
@@ -2,6 +2,8 @@ package app.alextran.immich
|
|||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
class MainActivity: FlutterActivity() {
|
||||||
|
|
||||||
@@ -10,4 +12,14 @@ class MainActivity: FlutterActivity() {
|
|||||||
flutterEngine.getPlugins().add(BackgroundServicePlugin())
|
flutterEngine.getPlugins().add(BackgroundServicePlugin())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
try {
|
||||||
|
startService(Intent(getBaseContext(), AppClearedService::class.java));
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// startService must not be called when app is in background (crashes app)
|
||||||
|
// there is nothing we can do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 37,
|
"android.injected.version.code" => 43,
|
||||||
"android.injected.version.name" => "1.27.0",
|
"android.injected.version.name" => "1.29.1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Fixed remove empty translations
|
||||||
|
* Fixed search page crashes the app on some Android models
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Improve Android background service reliability
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Fix background service cannot run in release build
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Fixed oversize play button on video
|
||||||
|
* Fixed app crashing when swipe between assets
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Fixed Android BackgroundServiceStartNotAllowedException
|
||||||
|
* Restore old cache mechanism
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Update deprecated API that causes notification not dismissing after background upload progress finished.
|
||||||
@@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000222">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00023">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="55.311329">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="58.722434">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.070842">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="36.768014">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
"album_viewer_appbar_share_leave": "Album verlassen",
|
"album_viewer_appbar_share_leave": "Album verlassen",
|
||||||
"album_viewer_appbar_share_remove": "Entferne vom Album",
|
"album_viewer_appbar_share_remove": "Entferne vom Album",
|
||||||
"album_viewer_page_share_add_users": "Nutzer hinzufügen",
|
"album_viewer_page_share_add_users": "Nutzer hinzufügen",
|
||||||
"asset_list_settings_subtitle": "",
|
|
||||||
"asset_list_settings_title": "",
|
|
||||||
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
|
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
|
||||||
"backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
|
"backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
|
||||||
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
|
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
|
||||||
@@ -21,26 +19,7 @@
|
|||||||
"backup_album_selection_page_selection_info": "Auswahl",
|
"backup_album_selection_page_selection_info": "Auswahl",
|
||||||
"backup_album_selection_page_total_assets": "Elemente",
|
"backup_album_selection_page_total_assets": "Elemente",
|
||||||
"backup_all": "Alle",
|
"backup_all": "Alle",
|
||||||
"backup_background_service_backup_failed_message": "",
|
|
||||||
"backup_background_service_connection_failed_message": "",
|
|
||||||
"backup_background_service_current_upload_notification": "",
|
|
||||||
"backup_background_service_default_notification": "",
|
|
||||||
"backup_background_service_error_title": "",
|
|
||||||
"backup_background_service_in_progress_notification": "",
|
|
||||||
"backup_background_service_upload_failure_notification": "",
|
|
||||||
"backup_controller_page_albums": "Gesicherte Alben",
|
"backup_controller_page_albums": "Gesicherte Alben",
|
||||||
"backup_controller_page_background_battery_info_link": "",
|
|
||||||
"backup_controller_page_background_battery_info_message": "",
|
|
||||||
"backup_controller_page_background_battery_info_ok": "",
|
|
||||||
"backup_controller_page_background_battery_info_title": "",
|
|
||||||
"backup_controller_page_background_charging": "",
|
|
||||||
"backup_controller_page_background_configure_error": "",
|
|
||||||
"backup_controller_page_background_description": "",
|
|
||||||
"backup_controller_page_background_is_off": "",
|
|
||||||
"backup_controller_page_background_is_on": "",
|
|
||||||
"backup_controller_page_background_turn_off": "",
|
|
||||||
"backup_controller_page_background_turn_on": "",
|
|
||||||
"backup_controller_page_background_wifi": "",
|
|
||||||
"backup_controller_page_backup": "Sicherung",
|
"backup_controller_page_backup": "Sicherung",
|
||||||
"backup_controller_page_backup_selected": "Ausgewählt: ",
|
"backup_controller_page_backup_selected": "Ausgewählt: ",
|
||||||
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
|
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
|
||||||
@@ -69,19 +48,6 @@
|
|||||||
"backup_controller_page_uploading_file_info": "Informationen",
|
"backup_controller_page_uploading_file_info": "Informationen",
|
||||||
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
|
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
|
||||||
"backup_info_card_assets": "Elemente",
|
"backup_info_card_assets": "Elemente",
|
||||||
"cache_settings_album_thumbnails": "",
|
|
||||||
"cache_settings_clear_cache_button": "",
|
|
||||||
"cache_settings_clear_cache_button_title": "",
|
|
||||||
"cache_settings_image_cache_size": "",
|
|
||||||
"cache_settings_statistics_album": "",
|
|
||||||
"cache_settings_statistics_assets": "",
|
|
||||||
"cache_settings_statistics_full": "",
|
|
||||||
"cache_settings_statistics_shared": "",
|
|
||||||
"cache_settings_statistics_thumbnail": "",
|
|
||||||
"cache_settings_statistics_title": "",
|
|
||||||
"cache_settings_subtitle": "",
|
|
||||||
"cache_settings_thumbnail_size": "",
|
|
||||||
"cache_settings_title": "",
|
|
||||||
"control_bottom_app_bar_delete": "Löschen",
|
"control_bottom_app_bar_delete": "Löschen",
|
||||||
"control_bottom_app_bar_share": "Teilen",
|
"control_bottom_app_bar_share": "Teilen",
|
||||||
"create_album_page_untitled": "Unbenannt",
|
"create_album_page_untitled": "Unbenannt",
|
||||||
@@ -127,13 +93,6 @@
|
|||||||
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
|
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
|
||||||
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
|
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
|
||||||
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||||
"setting_notifications_notify_failures_grace_period": "",
|
|
||||||
"setting_notifications_notify_hours": "",
|
|
||||||
"setting_notifications_notify_immediately": "",
|
|
||||||
"setting_notifications_notify_minutes": "",
|
|
||||||
"setting_notifications_notify_never": "",
|
|
||||||
"setting_notifications_subtitle": "",
|
|
||||||
"setting_notifications_title": "",
|
|
||||||
"setting_pages_app_bar_settings": "Einstellungen",
|
"setting_pages_app_bar_settings": "Einstellungen",
|
||||||
"share_add": "Hinzufügen",
|
"share_add": "Hinzufügen",
|
||||||
"share_add_photos": "Fotos hinzufügen",
|
"share_add_photos": "Fotos hinzufügen",
|
||||||
@@ -150,8 +109,6 @@
|
|||||||
"tab_controller_nav_photos": "Fotos",
|
"tab_controller_nav_photos": "Fotos",
|
||||||
"tab_controller_nav_search": "Suche",
|
"tab_controller_nav_search": "Suche",
|
||||||
"tab_controller_nav_sharing": "Teilen",
|
"tab_controller_nav_sharing": "Teilen",
|
||||||
"theme_setting_asset_list_storage_indicator_title": "",
|
|
||||||
"theme_setting_asset_list_tiles_per_row_title": "",
|
|
||||||
"theme_setting_dark_mode_switch": "Dunkler Modus",
|
"theme_setting_dark_mode_switch": "Dunkler Modus",
|
||||||
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
|
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
|
||||||
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
|
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
|
||||||
|
|||||||
@@ -21,12 +21,8 @@
|
|||||||
"backup_controller_page_backup_selected": "Seleccionado:",
|
"backup_controller_page_backup_selected": "Seleccionado:",
|
||||||
"backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos",
|
"backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos",
|
||||||
"backup_controller_page_cancel": "Cancelar",
|
"backup_controller_page_cancel": "Cancelar",
|
||||||
"backup_controller_page_created": "",
|
|
||||||
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.",
|
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.",
|
||||||
"backup_controller_page_excluded": "Excluido:",
|
"backup_controller_page_excluded": "Excluido:",
|
||||||
"backup_controller_page_failed": "",
|
|
||||||
"backup_controller_page_filename": "",
|
|
||||||
"backup_controller_page_id": "",
|
|
||||||
"backup_controller_page_info": "Información de la Copia de Seguridad",
|
"backup_controller_page_info": "Información de la Copia de Seguridad",
|
||||||
"backup_controller_page_none_selected": "Ninguno seleccionado",
|
"backup_controller_page_none_selected": "Ninguno seleccionado",
|
||||||
"backup_controller_page_remainder": "Remanente",
|
"backup_controller_page_remainder": "Remanente",
|
||||||
@@ -42,7 +38,6 @@
|
|||||||
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
|
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
|
||||||
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
|
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
|
||||||
"backup_controller_page_turn_on": "Activar la copia de seguridad",
|
"backup_controller_page_turn_on": "Activar la copia de seguridad",
|
||||||
"backup_controller_page_uploading_file_info": "",
|
|
||||||
"backup_err_only_album": "No se puede eliminar el único álbum",
|
"backup_err_only_album": "No se puede eliminar el único álbum",
|
||||||
"backup_info_card_assets": "activos",
|
"backup_info_card_assets": "activos",
|
||||||
"control_bottom_app_bar_delete": "Eliminar",
|
"control_bottom_app_bar_delete": "Eliminar",
|
||||||
@@ -67,7 +62,6 @@
|
|||||||
"login_form_err_invalid_email": "Correo electrónico no válido",
|
"login_form_err_invalid_email": "Correo electrónico no válido",
|
||||||
"login_form_err_leading_whitespace": "Espacio en blanco inicial",
|
"login_form_err_leading_whitespace": "Espacio en blanco inicial",
|
||||||
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
|
"login_form_err_trailing_whitespace": "Espacio en blanco al final",
|
||||||
"login_form_failed_login": "",
|
|
||||||
"login_form_label_email": "Correo",
|
"login_form_label_email": "Correo",
|
||||||
"login_form_label_password": "Contraseña",
|
"login_form_label_password": "Contraseña",
|
||||||
"login_form_password_hint": "contraseña",
|
"login_form_password_hint": "contraseña",
|
||||||
@@ -76,14 +70,12 @@
|
|||||||
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
|
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados",
|
||||||
"profile_drawer_sign_out": "Cerrar Sesión",
|
"profile_drawer_sign_out": "Cerrar Sesión",
|
||||||
"search_bar_hint": "Busca tus fotos",
|
"search_bar_hint": "Busca tus fotos",
|
||||||
"search_page_no_objects": "",
|
|
||||||
"search_page_no_places": "No hay información de lugares disponibles",
|
"search_page_no_places": "No hay información de lugares disponibles",
|
||||||
"search_page_places": "Lugares",
|
"search_page_places": "Lugares",
|
||||||
"search_page_things": "Cosas",
|
"search_page_things": "Cosas",
|
||||||
"search_result_page_new_search_hint": "Nueva Busqueda",
|
"search_result_page_new_search_hint": "Nueva Busqueda",
|
||||||
"select_additional_user_for_sharing_page_suggestions": "Sugerencias",
|
"select_additional_user_for_sharing_page_suggestions": "Sugerencias",
|
||||||
"select_user_for_sharing_page_err_album": "Fallo al crear el álbum",
|
"select_user_for_sharing_page_err_album": "Fallo al crear el álbum",
|
||||||
"select_user_for_sharing_page_share_suggestions": "",
|
|
||||||
"share_add": "Añadir",
|
"share_add": "Añadir",
|
||||||
"share_add_photos": "Añadir fotos",
|
"share_add_photos": "Añadir fotos",
|
||||||
"share_add_title": "Añadir un título",
|
"share_add_title": "Añadir un título",
|
||||||
|
|||||||
@@ -49,9 +49,6 @@
|
|||||||
"create_shared_album_page_share": "Jaa",
|
"create_shared_album_page_share": "Jaa",
|
||||||
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
|
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
|
||||||
"create_shared_album_page_share_select_photos": "Valitse kuvat",
|
"create_shared_album_page_share_select_photos": "Valitse kuvat",
|
||||||
"daily_title_text_date": "",
|
|
||||||
"daily_title_text_date_year": "",
|
|
||||||
"date_format": "",
|
|
||||||
"delete_dialog_alert": "Nämä kohteet poistetaan pysyvästi Immich:stä ja laitteeltasi",
|
"delete_dialog_alert": "Nämä kohteet poistetaan pysyvästi Immich:stä ja laitteeltasi",
|
||||||
"delete_dialog_cancel": "Peruuta",
|
"delete_dialog_cancel": "Peruuta",
|
||||||
"delete_dialog_ok": "Poista",
|
"delete_dialog_ok": "Poista",
|
||||||
@@ -72,7 +69,6 @@
|
|||||||
"login_form_label_password": "Salasana",
|
"login_form_label_password": "Salasana",
|
||||||
"login_form_password_hint": "salasana",
|
"login_form_password_hint": "salasana",
|
||||||
"login_form_save_login": "Pysy kirjautuneena",
|
"login_form_save_login": "Pysy kirjautuneena",
|
||||||
"monthly_title_text_date_format": "",
|
|
||||||
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
|
"profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla",
|
||||||
"profile_drawer_sign_out": "Kirjaudu ulos",
|
"profile_drawer_sign_out": "Kirjaudu ulos",
|
||||||
"search_bar_hint": "Etsi kuvia",
|
"search_bar_hint": "Etsi kuvia",
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
"album_viewer_appbar_share_leave": "Quitter l'album",
|
"album_viewer_appbar_share_leave": "Quitter l'album",
|
||||||
"album_viewer_appbar_share_remove": "Retirer de l'album",
|
"album_viewer_appbar_share_remove": "Retirer de l'album",
|
||||||
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
|
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
|
||||||
"asset_list_settings_subtitle": "",
|
|
||||||
"asset_list_settings_title": "",
|
|
||||||
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
|
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
|
||||||
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
|
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
|
||||||
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
|
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
|
||||||
@@ -21,26 +19,7 @@
|
|||||||
"backup_album_selection_page_selection_info": "Informations sur la sélection",
|
"backup_album_selection_page_selection_info": "Informations sur la sélection",
|
||||||
"backup_album_selection_page_total_assets": "Total des éléments uniques",
|
"backup_album_selection_page_total_assets": "Total des éléments uniques",
|
||||||
"backup_all": "Tout",
|
"backup_all": "Tout",
|
||||||
"backup_background_service_backup_failed_message": "",
|
|
||||||
"backup_background_service_connection_failed_message": "",
|
|
||||||
"backup_background_service_current_upload_notification": "",
|
|
||||||
"backup_background_service_default_notification": "",
|
|
||||||
"backup_background_service_error_title": "",
|
|
||||||
"backup_background_service_in_progress_notification": "",
|
|
||||||
"backup_background_service_upload_failure_notification": "",
|
|
||||||
"backup_controller_page_albums": "Sauvegarder les albums",
|
"backup_controller_page_albums": "Sauvegarder les albums",
|
||||||
"backup_controller_page_background_battery_info_link": "",
|
|
||||||
"backup_controller_page_background_battery_info_message": "",
|
|
||||||
"backup_controller_page_background_battery_info_ok": "",
|
|
||||||
"backup_controller_page_background_battery_info_title": "",
|
|
||||||
"backup_controller_page_background_charging": "",
|
|
||||||
"backup_controller_page_background_configure_error": "",
|
|
||||||
"backup_controller_page_background_description": "",
|
|
||||||
"backup_controller_page_background_is_off": "",
|
|
||||||
"backup_controller_page_background_is_on": "",
|
|
||||||
"backup_controller_page_background_turn_off": "",
|
|
||||||
"backup_controller_page_background_turn_on": "",
|
|
||||||
"backup_controller_page_background_wifi": "",
|
|
||||||
"backup_controller_page_backup": "Sauvegardé",
|
"backup_controller_page_backup": "Sauvegardé",
|
||||||
"backup_controller_page_backup_selected": "Sélectionné : ",
|
"backup_controller_page_backup_selected": "Sélectionné : ",
|
||||||
"backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
|
"backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
|
||||||
@@ -69,19 +48,6 @@
|
|||||||
"backup_controller_page_uploading_file_info": "Envoi d'informations sur le fichier",
|
"backup_controller_page_uploading_file_info": "Envoi d'informations sur le fichier",
|
||||||
"backup_err_only_album": "Impossible de retirer le seul album",
|
"backup_err_only_album": "Impossible de retirer le seul album",
|
||||||
"backup_info_card_assets": "éléments",
|
"backup_info_card_assets": "éléments",
|
||||||
"cache_settings_album_thumbnails": "",
|
|
||||||
"cache_settings_clear_cache_button": "",
|
|
||||||
"cache_settings_clear_cache_button_title": "",
|
|
||||||
"cache_settings_image_cache_size": "",
|
|
||||||
"cache_settings_statistics_album": "",
|
|
||||||
"cache_settings_statistics_assets": "",
|
|
||||||
"cache_settings_statistics_full": "",
|
|
||||||
"cache_settings_statistics_shared": "",
|
|
||||||
"cache_settings_statistics_thumbnail": "",
|
|
||||||
"cache_settings_statistics_title": "",
|
|
||||||
"cache_settings_subtitle": "",
|
|
||||||
"cache_settings_thumbnail_size": "",
|
|
||||||
"cache_settings_title": "",
|
|
||||||
"control_bottom_app_bar_delete": "Supprimer",
|
"control_bottom_app_bar_delete": "Supprimer",
|
||||||
"control_bottom_app_bar_share": "Partager",
|
"control_bottom_app_bar_share": "Partager",
|
||||||
"create_album_page_untitled": "Sans titre",
|
"create_album_page_untitled": "Sans titre",
|
||||||
@@ -127,14 +93,6 @@
|
|||||||
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
||||||
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
|
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
|
||||||
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||||
"setting_notifications_notify_failures_grace_period": "",
|
|
||||||
"setting_notifications_notify_hours": "",
|
|
||||||
"setting_notifications_notify_immediately": "",
|
|
||||||
"setting_notifications_notify_minutes": "",
|
|
||||||
"setting_notifications_notify_never": "",
|
|
||||||
"setting_notifications_subtitle": "",
|
|
||||||
"setting_notifications_title": "",
|
|
||||||
"setting_pages_app_bar_settings": "",
|
|
||||||
"share_add": "Ajouter",
|
"share_add": "Ajouter",
|
||||||
"share_add_photos": "Ajouter des photos",
|
"share_add_photos": "Ajouter des photos",
|
||||||
"share_add_title": "Ajouter un titre",
|
"share_add_title": "Ajouter un titre",
|
||||||
@@ -150,16 +108,6 @@
|
|||||||
"tab_controller_nav_photos": "Photos",
|
"tab_controller_nav_photos": "Photos",
|
||||||
"tab_controller_nav_search": "Recherche",
|
"tab_controller_nav_search": "Recherche",
|
||||||
"tab_controller_nav_sharing": "Partage",
|
"tab_controller_nav_sharing": "Partage",
|
||||||
"theme_setting_asset_list_storage_indicator_title": "",
|
|
||||||
"theme_setting_asset_list_tiles_per_row_title": "",
|
|
||||||
"theme_setting_dark_mode_switch": "",
|
|
||||||
"theme_setting_image_viewer_quality_subtitle": "",
|
|
||||||
"theme_setting_image_viewer_quality_title": "",
|
|
||||||
"theme_setting_system_theme_switch": "",
|
|
||||||
"theme_setting_theme_subtitle": "",
|
|
||||||
"theme_setting_theme_title": "",
|
|
||||||
"theme_setting_three_stage_loading_subtitle": "",
|
|
||||||
"theme_setting_three_stage_loading_title": "",
|
|
||||||
"version_announcement_overlay_ack": "Confirmer",
|
"version_announcement_overlay_ack": "Confirmer",
|
||||||
"version_announcement_overlay_release_notes": "notes de mise à jour",
|
"version_announcement_overlay_release_notes": "notes de mise à jour",
|
||||||
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
|
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
|
||||||
|
|||||||
@@ -360,11 +360,11 @@
|
|||||||
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 = 52;
|
CURRENT_PROJECT_VERSION = 58;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -495,11 +495,11 @@
|
|||||||
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 = 52;
|
CURRENT_PROJECT_VERSION = 58;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -522,11 +522,11 @@
|
|||||||
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 = 52;
|
CURRENT_PROJECT_VERSION = 58;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.27.0</string>
|
<string>1.30.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>52</string>
|
<string>58</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true />
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
|
|||||||
@@ -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.27.0"
|
version_number: "1.29.1"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -5,32 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000173">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.499192">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.412813">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="30.057077">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="3.69289">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.438506">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.408563">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="91.259106">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="65.350555">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="102.092139">
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="68.894733">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/services/cache.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -15,11 +14,9 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
const AlbumThumbnailCard({
|
const AlbumThumbnailCard({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.album,
|
required this.album,
|
||||||
required this.cacheService,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final AlbumResponseDto album;
|
final AlbumResponseDto album;
|
||||||
final CacheService cacheService;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -39,7 +36,6 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
cacheManager: cacheService.getCache(CacheType.albumThumbnail),
|
|
||||||
memCacheHeight: max(400, cardSize.toInt() * 3),
|
memCacheHeight: max(400, cardSize.toInt() * 3),
|
||||||
width: cardSize,
|
width: cardSize,
|
||||||
height: cardSize,
|
height: cardSize,
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/services/cache.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -17,13 +14,11 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
final AssetResponseDto asset;
|
final AssetResponseDto asset;
|
||||||
final List<AssetResponseDto> assetList;
|
final List<AssetResponseDto> assetList;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final BaseCacheManager? cacheManager;
|
|
||||||
|
|
||||||
const AlbumViewerThumbnail({
|
const AlbumViewerThumbnail({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.assetList,
|
required this.assetList,
|
||||||
this.cacheManager,
|
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -126,7 +121,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(border: drawBorderColor()),
|
decoration: BoxDecoration(border: drawBorderColor()),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
cacheManager: cacheManager,
|
|
||||||
cacheKey: asset.id,
|
cacheKey: asset.id,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/cache.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -24,7 +22,6 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
var newAssetsForAlbum =
|
var newAssetsForAlbum =
|
||||||
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
||||||
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||||
final cacheService = ref.watch(cacheServiceProvider);
|
|
||||||
|
|
||||||
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
||||||
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
@@ -114,7 +111,6 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(border: drawBorderColor()),
|
decoration: BoxDecoration(border: drawBorderColor()),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
cacheManager: cacheService.getCache(CacheType.thumbnail),
|
|
||||||
cacheKey: asset.id,
|
cacheKey: asset.id,
|
||||||
width: 150,
|
width: 150,
|
||||||
height: 150,
|
height: 150,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/shared/services/cache.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -16,7 +14,6 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final cacheService = ref.watch(cacheServiceProvider);
|
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -26,7 +23,6 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
cacheManager: cacheService.getCache(CacheType.thumbnail),
|
|
||||||
cacheKey: asset.id,
|
cacheKey: asset.id,
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 500,
|
height: 500,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
|
|||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/services/cache.service.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
@@ -192,7 +191,6 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
final bool showStorageIndicator =
|
final bool showStorageIndicator =
|
||||||
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
|
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
|
||||||
final cacheService = ref.watch(cacheServiceProvider);
|
|
||||||
|
|
||||||
if (albumInfo.assets.isNotEmpty) {
|
if (albumInfo.assets.isNotEmpty) {
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
@@ -207,7 +205,6 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return AlbumViewerThumbnail(
|
return AlbumViewerThumbnail(
|
||||||
cacheManager: cacheService.getCache(CacheType.thumbnail),
|
|
||||||
asset: albumInfo.assets[index],
|
asset: albumInfo.assets[index],
|
||||||
assetList: albumInfo.assets,
|
assetList: albumInfo.assets,
|
||||||
showStorageIndicator: showStorageIndicator,
|
showStorageIndicator: showStorageIndicator,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/services/cache.service.dart';
|
|
||||||
|
|
||||||
class LibraryPage extends HookConsumerWidget {
|
class LibraryPage extends HookConsumerWidget {
|
||||||
const LibraryPage({Key? key}) : super(key: key);
|
const LibraryPage({Key? key}) : super(key: key);
|
||||||
@@ -14,7 +13,6 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final albums = ref.watch(albumProvider);
|
final albums = ref.watch(albumProvider);
|
||||||
final cacheService = ref.watch(cacheServiceProvider);
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@@ -104,7 +102,6 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
_buildCreateAlbumButton(),
|
_buildCreateAlbumButton(),
|
||||||
for (var album in albums)
|
for (var album in albums)
|
||||||
AlbumThumbnailCard(
|
AlbumThumbnailCard(
|
||||||
cacheService: cacheService,
|
|
||||||
album: album,
|
album: album,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import 'package:immich_mobile/constants/hive_box.dart';
|
|||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
|
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/services/cache.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
||||||
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
final CacheService cacheService = ref.watch(cacheServiceProvider);
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@@ -47,8 +45,6 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
height: 60,
|
height: 60,
|
||||||
memCacheHeight: 200,
|
memCacheHeight: 200,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
cacheManager:
|
|
||||||
cacheService.getCache(CacheType.sharedAlbumThumbnail),
|
|
||||||
imageUrl: getAlbumThumbnailUrl(album),
|
imageUrl: getAlbumThumbnailUrl(album),
|
||||||
cacheKey: album.albumThumbnailAssetId,
|
cacheKey: album.albumThumbnailAssetId,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
enum _RemoteImageStatus { empty, thumbnail, preview, full }
|
enum _RemoteImageStatus { empty, thumbnail, preview, full }
|
||||||
@@ -12,6 +10,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
bool _zoomedIn = false;
|
bool _zoomedIn = false;
|
||||||
|
|
||||||
static const int swipeThreshold = 100;
|
static const int swipeThreshold = 100;
|
||||||
|
late CachedNetworkImageProvider fullProvider;
|
||||||
|
late CachedNetworkImageProvider previewProvider;
|
||||||
|
late CachedNetworkImageProvider thumbnailProvider;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -56,21 +57,14 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
widget.isZoomedFunction();
|
widget.isZoomedFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fireStartLoadingEvent() {
|
|
||||||
widget.onLoadingStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _fireFinishedLoadingEvent() {
|
|
||||||
widget.onLoadingCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
CachedNetworkImageProvider _authorizedImageProvider(
|
CachedNetworkImageProvider _authorizedImageProvider(
|
||||||
String url, String cacheKey, BaseCacheManager? cacheManager) {
|
String url,
|
||||||
|
String cacheKey,
|
||||||
|
) {
|
||||||
return CachedNetworkImageProvider(
|
return CachedNetworkImageProvider(
|
||||||
url,
|
url,
|
||||||
headers: {"Authorization": widget.authToken},
|
headers: {"Authorization": widget.authToken},
|
||||||
cacheKey: cacheKey,
|
cacheKey: cacheKey,
|
||||||
cacheManager: cacheManager,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +85,6 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (newStatus != _RemoteImageStatus.full) {
|
|
||||||
_fireStartLoadingEvent();
|
|
||||||
} else {
|
|
||||||
_fireFinishedLoadingEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_status = newStatus;
|
_status = newStatus;
|
||||||
_imageProvider = provider;
|
_imageProvider = provider;
|
||||||
@@ -104,10 +92,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _loadImages() {
|
void _loadImages() {
|
||||||
CachedNetworkImageProvider thumbnailProvider = _authorizedImageProvider(
|
thumbnailProvider = _authorizedImageProvider(
|
||||||
widget.thumbnailUrl,
|
widget.thumbnailUrl,
|
||||||
widget.cacheKey,
|
widget.cacheKey,
|
||||||
widget.thumbnailCacheManager,
|
|
||||||
);
|
);
|
||||||
_imageProvider = thumbnailProvider;
|
_imageProvider = thumbnailProvider;
|
||||||
|
|
||||||
@@ -121,10 +108,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (widget.previewUrl != null) {
|
if (widget.previewUrl != null) {
|
||||||
CachedNetworkImageProvider previewProvider = _authorizedImageProvider(
|
previewProvider = _authorizedImageProvider(
|
||||||
widget.previewUrl!,
|
widget.previewUrl!,
|
||||||
"${widget.cacheKey}_previewStage",
|
"${widget.cacheKey}_previewStage",
|
||||||
widget.previewCacheManager,
|
|
||||||
);
|
);
|
||||||
previewProvider.resolve(const ImageConfiguration()).addListener(
|
previewProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
@@ -133,10 +119,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CachedNetworkImageProvider fullProvider = _authorizedImageProvider(
|
fullProvider = _authorizedImageProvider(
|
||||||
widget.imageUrl,
|
widget.imageUrl,
|
||||||
"${widget.cacheKey}_fullStage",
|
"${widget.cacheKey}_fullStage",
|
||||||
widget.fullCacheManager,
|
|
||||||
);
|
);
|
||||||
fullProvider.resolve(const ImageConfiguration()).addListener(
|
fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
@@ -147,8 +132,23 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_loadImages();
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_loadImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() async {
|
||||||
|
super.dispose();
|
||||||
|
|
||||||
|
if (_status == _RemoteImageStatus.full) {
|
||||||
|
await fullProvider.evict();
|
||||||
|
} else if (_status == _RemoteImageStatus.preview) {
|
||||||
|
await previewProvider.evict();
|
||||||
|
} else if (_status == _RemoteImageStatus.thumbnail) {
|
||||||
|
await thumbnailProvider.evict();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _imageProvider.evict();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,11 +163,6 @@ class RemotePhotoView extends StatefulWidget {
|
|||||||
required this.onSwipeDown,
|
required this.onSwipeDown,
|
||||||
required this.onSwipeUp,
|
required this.onSwipeUp,
|
||||||
this.previewUrl,
|
this.previewUrl,
|
||||||
required this.onLoadingCompleted,
|
|
||||||
required this.onLoadingStart,
|
|
||||||
this.thumbnailCacheManager,
|
|
||||||
this.previewCacheManager,
|
|
||||||
this.fullCacheManager,
|
|
||||||
required this.cacheKey,
|
required this.cacheKey,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -175,11 +170,6 @@ class RemotePhotoView extends StatefulWidget {
|
|||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final String authToken;
|
final String authToken;
|
||||||
final String? previewUrl;
|
final String? previewUrl;
|
||||||
final Function onLoadingCompleted;
|
|
||||||
final Function onLoadingStart;
|
|
||||||
final BaseCacheManager? thumbnailCacheManager;
|
|
||||||
final BaseCacheManager? previewCacheManager;
|
|
||||||
final BaseCacheManager? fullCacheManager;
|
|
||||||
final String cacheKey;
|
final String cacheKey;
|
||||||
|
|
||||||
final void Function() onSwipeDown;
|
final void Function() onSwipeDown;
|
||||||
|
|||||||
@@ -24,11 +24,9 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
double iconSize = 18.0;
|
double iconSize = 18.0;
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
// iconTheme: IconThemeData(color: Colors.grey[100]),
|
|
||||||
// actionsIconTheme: IconThemeData(color: Colors.grey[100]),
|
|
||||||
foregroundColor: Colors.grey[100],
|
foregroundColor: Colors.grey[100],
|
||||||
toolbarHeight: 60,
|
toolbarHeight: 60,
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.transparent,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).pop();
|
AutoRouter.of(context).pop();
|
||||||
|
|||||||
@@ -121,8 +121,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
||||||
isZoomedFunction: isZoomedMethod,
|
isZoomedFunction: isZoomedMethod,
|
||||||
isZoomedListener: isZoomedListener,
|
isZoomedListener: isZoomedListener,
|
||||||
onLoadingCompleted: () => {},
|
|
||||||
onLoadingStart: () => {},
|
|
||||||
asset: assetList[index],
|
asset: assetList[index],
|
||||||
heroTag: assetList[index].id,
|
heroTag: assetList[index].id,
|
||||||
threeStageLoading: threeStageLoading.value,
|
threeStageLoading: threeStageLoading.value,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/cache.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -19,8 +18,6 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
final String authToken;
|
final String authToken;
|
||||||
final ValueNotifier<bool> isZoomedListener;
|
final ValueNotifier<bool> isZoomedListener;
|
||||||
final void Function() isZoomedFunction;
|
final void Function() isZoomedFunction;
|
||||||
final void Function() onLoadingCompleted;
|
|
||||||
final void Function() onLoadingStart;
|
|
||||||
final bool threeStageLoading;
|
final bool threeStageLoading;
|
||||||
|
|
||||||
ImageViewerPage({
|
ImageViewerPage({
|
||||||
@@ -30,8 +27,6 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
required this.authToken,
|
required this.authToken,
|
||||||
required this.isZoomedFunction,
|
required this.isZoomedFunction,
|
||||||
required this.isZoomedListener,
|
required this.isZoomedListener,
|
||||||
required this.onLoadingCompleted,
|
|
||||||
required this.onLoadingStart,
|
|
||||||
required this.threeStageLoading,
|
required this.threeStageLoading,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -41,7 +36,6 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final downloadAssetStatus =
|
final downloadAssetStatus =
|
||||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
final cacheService = ref.watch(cacheServiceProvider);
|
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail =
|
assetDetail =
|
||||||
@@ -85,14 +79,6 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
isZoomedListener: isZoomedListener,
|
isZoomedListener: isZoomedListener,
|
||||||
onSwipeDown: () => AutoRouter.of(context).pop(),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
onSwipeUp: () => showInfo(),
|
onSwipeUp: () => showInfo(),
|
||||||
onLoadingCompleted: onLoadingCompleted,
|
|
||||||
onLoadingStart: onLoadingStart,
|
|
||||||
thumbnailCacheManager:
|
|
||||||
cacheService.getCache(CacheType.thumbnail),
|
|
||||||
previewCacheManager:
|
|
||||||
cacheService.getCache(CacheType.imageViewerPreview),
|
|
||||||
fullCacheManager:
|
|
||||||
cacheService.getCache(CacheType.imageViewerFull),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
|||||||
_createChewieController() {
|
_createChewieController() {
|
||||||
chewieController = ChewieController(
|
chewieController = ChewieController(
|
||||||
showOptions: true,
|
showOptions: true,
|
||||||
showControlsOnInitialize: true,
|
showControlsOnInitialize: false,
|
||||||
videoPlayerController: videoPlayerController,
|
videoPlayerController: videoPlayerController,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
autoInitialize: true,
|
autoInitialize: true,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
|||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'dart:ui' show IsolateNameServer, PluginUtilities;
|
import 'dart:ui' show IsolateNameServer, PluginUtilities;
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@@ -33,7 +32,6 @@ class BackgroundService {
|
|||||||
MethodChannel('immich/foregroundChannel');
|
MethodChannel('immich/foregroundChannel');
|
||||||
static const MethodChannel _backgroundChannel =
|
static const MethodChannel _backgroundChannel =
|
||||||
MethodChannel('immich/backgroundChannel');
|
MethodChannel('immich/backgroundChannel');
|
||||||
bool _isForegroundInitialized = false;
|
|
||||||
bool _isBackgroundInitialized = false;
|
bool _isBackgroundInitialized = false;
|
||||||
CancellationToken? _cancellationToken;
|
CancellationToken? _cancellationToken;
|
||||||
bool _canceledBySystem = false;
|
bool _canceledBySystem = false;
|
||||||
@@ -43,32 +41,34 @@ class BackgroundService {
|
|||||||
ReceivePort? _rp;
|
ReceivePort? _rp;
|
||||||
bool _errorGracePeriodExceeded = true;
|
bool _errorGracePeriodExceeded = true;
|
||||||
|
|
||||||
bool get isForegroundInitialized {
|
|
||||||
return _isForegroundInitialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isBackgroundInitialized {
|
bool get isBackgroundInitialized {
|
||||||
return _isBackgroundInitialized;
|
return _isBackgroundInitialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _initialize() async {
|
|
||||||
final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
|
|
||||||
var result = await _foregroundChannel
|
|
||||||
.invokeMethod('initialize', [callback.toRawHandle()]);
|
|
||||||
_isForegroundInitialized = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensures that the background service is enqueued if enabled in settings
|
/// Ensures that the background service is enqueued if enabled in settings
|
||||||
Future<bool> resumeServiceIfEnabled() async {
|
Future<bool> resumeServiceIfEnabled() async {
|
||||||
return await isBackgroundBackupEnabled() &&
|
return await isBackgroundBackupEnabled() && await enableService();
|
||||||
await startService(keepExisting: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enqueues the background service
|
/// Enqueues the background service
|
||||||
Future<bool> startService({
|
Future<bool> enableService({bool immediate = false}) async {
|
||||||
bool immediate = false,
|
if (!Platform.isAndroid) {
|
||||||
bool keepExisting = false,
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
|
||||||
|
final String title =
|
||||||
|
"backup_background_service_default_notification".tr();
|
||||||
|
final bool ok = await _foregroundChannel
|
||||||
|
.invokeMethod('enable', [callback.toRawHandle(), title, immediate]);
|
||||||
|
return ok;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configures the background service
|
||||||
|
Future<bool> configureService({
|
||||||
bool requireUnmetered = true,
|
bool requireUnmetered = true,
|
||||||
bool requireCharging = false,
|
bool requireCharging = false,
|
||||||
}) async {
|
}) async {
|
||||||
@@ -76,14 +76,9 @@ class BackgroundService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!_isForegroundInitialized) {
|
|
||||||
await _initialize();
|
|
||||||
}
|
|
||||||
final String title =
|
|
||||||
"backup_background_service_default_notification".tr();
|
|
||||||
final bool ok = await _foregroundChannel.invokeMethod(
|
final bool ok = await _foregroundChannel.invokeMethod(
|
||||||
'start',
|
'configure',
|
||||||
[immediate, keepExisting, requireUnmetered, requireCharging, title],
|
[requireUnmetered, requireCharging],
|
||||||
);
|
);
|
||||||
return ok;
|
return ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -92,15 +87,12 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Cancels the background service (if currently running) and removes it from work queue
|
/// Cancels the background service (if currently running) and removes it from work queue
|
||||||
Future<bool> stopService() async {
|
Future<bool> disableService() async {
|
||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!_isForegroundInitialized) {
|
final ok = await _foregroundChannel.invokeMethod('disable');
|
||||||
await _initialize();
|
|
||||||
}
|
|
||||||
final ok = await _foregroundChannel.invokeMethod('stop');
|
|
||||||
return ok;
|
return ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
@@ -113,9 +105,6 @@ class BackgroundService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!_isForegroundInitialized) {
|
|
||||||
await _initialize();
|
|
||||||
}
|
|
||||||
return await _foregroundChannel.invokeMethod("isEnabled");
|
return await _foregroundChannel.invokeMethod("isEnabled");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
@@ -128,9 +117,6 @@ class BackgroundService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!_isForegroundInitialized) {
|
|
||||||
await _initialize();
|
|
||||||
}
|
|
||||||
return await _foregroundChannel
|
return await _foregroundChannel
|
||||||
.invokeMethod('isIgnoringBatteryOptimizations');
|
.invokeMethod('isIgnoringBatteryOptimizations');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -187,7 +173,8 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"[_clearErrorNotifications] failed to communicate with plugin");
|
"[_clearErrorNotifications] failed to communicate with plugin",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -289,18 +276,11 @@ class BackgroundService {
|
|||||||
try {
|
try {
|
||||||
final bool hasAccess = await acquireLock();
|
final bool hasAccess = await acquireLock();
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
debugPrint("[_callHandler] could acquire lock, exiting");
|
debugPrint("[_callHandler] could not acquire lock, exiting");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await translationsLoaded;
|
await translationsLoaded;
|
||||||
final bool ok = await _onAssetsChanged();
|
final bool ok = await _onAssetsChanged();
|
||||||
if (ok) {
|
|
||||||
Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
|
|
||||||
} else if (Hive.box(backgroundBackupInfoBox).get(backupFailedSince) ==
|
|
||||||
null) {
|
|
||||||
Hive.box(backgroundBackupInfoBox)
|
|
||||||
.put(backupFailedSince, DateTime.now());
|
|
||||||
}
|
|
||||||
return ok;
|
return ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint(error.toString());
|
debugPrint(error.toString());
|
||||||
@@ -343,6 +323,31 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await PhotoManager.setIgnorePermissionCheck(true);
|
await PhotoManager.setIgnorePermissionCheck(true);
|
||||||
|
|
||||||
|
do {
|
||||||
|
final bool backupOk = await _runBackup(backupService, backupAlbumInfo);
|
||||||
|
if (backupOk) {
|
||||||
|
await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
|
||||||
|
await box.put(
|
||||||
|
backupInfoKey,
|
||||||
|
backupAlbumInfo,
|
||||||
|
);
|
||||||
|
} else if (Hive.box(backgroundBackupInfoBox).get(backupFailedSince) ==
|
||||||
|
null) {
|
||||||
|
Hive.box(backgroundBackupInfoBox)
|
||||||
|
.put(backupFailedSince, DateTime.now());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// check for new assets added while performing backup
|
||||||
|
} while (true ==
|
||||||
|
await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _runBackup(
|
||||||
|
BackupService backupService,
|
||||||
|
HiveBackupAlbums backupAlbumInfo,
|
||||||
|
) async {
|
||||||
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
|
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
|
||||||
|
|
||||||
if (_canceledBySystem) {
|
if (_canceledBySystem) {
|
||||||
@@ -382,10 +387,6 @@ class BackgroundService {
|
|||||||
);
|
);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
_clearErrorNotifications();
|
_clearErrorNotifications();
|
||||||
await box.put(
|
|
||||||
backupInfoKey,
|
|
||||||
backupAlbumInfo,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
_showErrorNotification(
|
_showErrorNotification(
|
||||||
title: "backup_background_service_error_title".tr(),
|
title: "backup_background_service_error_title".tr(),
|
||||||
@@ -447,6 +448,7 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
||||||
|
@pragma('vm:entry-point')
|
||||||
void _nativeEntry() {
|
void _nativeEntry() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
BackgroundService backgroundService = BackgroundService();
|
BackgroundService backgroundService = BackgroundService();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class AvailableAlbum {
|
|||||||
|
|
||||||
String get name => albumEntity.name;
|
String get name => albumEntity.name;
|
||||||
|
|
||||||
int get assetCount => albumEntity.assetCount;
|
Future<int> get assetCount => albumEntity.assetCountAsync;
|
||||||
|
|
||||||
String get id => albumEntity.id;
|
String get id => albumEntity.id;
|
||||||
|
|
||||||
|
|||||||
@@ -131,13 +131,15 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (state.backgroundBackup) {
|
if (state.backgroundBackup) {
|
||||||
|
bool success = true;
|
||||||
if (!wasEnabled) {
|
if (!wasEnabled) {
|
||||||
if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
|
if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
|
||||||
onBatteryInfo();
|
onBatteryInfo();
|
||||||
}
|
}
|
||||||
|
success &= await _backgroundService.enableService(immediate: true);
|
||||||
}
|
}
|
||||||
final bool success = await _backgroundService.stopService() &&
|
success &= success &&
|
||||||
await _backgroundService.startService(
|
await _backgroundService.configureService(
|
||||||
requireUnmetered: state.backupRequireWifi,
|
requireUnmetered: state.backupRequireWifi,
|
||||||
requireCharging: state.backupRequireCharging,
|
requireCharging: state.backupRequireCharging,
|
||||||
);
|
);
|
||||||
@@ -155,7 +157,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
onError("backup_controller_page_background_configure_error");
|
onError("backup_controller_page_background_configure_error");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final bool success = await _backgroundService.stopService();
|
final bool success = await _backgroundService.disableService();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
state = state.copyWith(backgroundBackup: wasEnabled);
|
state = state.copyWith(backgroundBackup: wasEnabled);
|
||||||
onError("backup_controller_page_background_configure_error");
|
onError("backup_controller_page_background_configure_error");
|
||||||
@@ -181,17 +183,21 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
for (AssetPathEntity album in albums) {
|
for (AssetPathEntity album in albums) {
|
||||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||||
|
|
||||||
var assetList =
|
var assetCountInAlbum = await album.assetCountAsync;
|
||||||
await album.getAssetListRange(start: 0, end: album.assetCount);
|
if (assetCountInAlbum > 0) {
|
||||||
|
var assetList =
|
||||||
|
await album.getAssetListRange(start: 0, end: assetCountInAlbum);
|
||||||
|
|
||||||
if (assetList.isNotEmpty) {
|
if (assetList.isNotEmpty) {
|
||||||
var thumbnailAsset = assetList.first;
|
var thumbnailAsset = assetList.first;
|
||||||
var thumbnailData = await thumbnailAsset
|
var thumbnailData = await thumbnailAsset
|
||||||
.thumbnailDataWithSize(const ThumbnailSize(512, 512));
|
.thumbnailDataWithSize(const ThumbnailSize(512, 512));
|
||||||
availableAlbum = availableAlbum.copyWith(thumbnailData: thumbnailData);
|
availableAlbum =
|
||||||
|
availableAlbum.copyWith(thumbnailData: thumbnailData);
|
||||||
|
}
|
||||||
|
|
||||||
|
availableAlbums.add(availableAlbum);
|
||||||
}
|
}
|
||||||
|
|
||||||
availableAlbums.add(availableAlbum);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(availableAlbums: availableAlbums);
|
state = state.copyWith(availableAlbums: availableAlbums);
|
||||||
@@ -294,14 +300,18 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
||||||
|
|
||||||
for (var album in state.selectedBackupAlbums) {
|
for (var album in state.selectedBackupAlbums) {
|
||||||
var assets = await album.albumEntity
|
var assets = await album.albumEntity.getAssetListRange(
|
||||||
.getAssetListRange(start: 0, end: album.assetCount);
|
start: 0,
|
||||||
|
end: await album.albumEntity.assetCountAsync,
|
||||||
|
);
|
||||||
assetsFromSelectedAlbums.addAll(assets);
|
assetsFromSelectedAlbums.addAll(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var album in state.excludedBackupAlbums) {
|
for (var album in state.excludedBackupAlbums) {
|
||||||
var assets = await album.albumEntity
|
var assets = await album.albumEntity.getAssetListRange(
|
||||||
.getAssetListRange(start: 0, end: album.assetCount);
|
start: 0,
|
||||||
|
end: await album.albumEntity.assetCountAsync,
|
||||||
|
);
|
||||||
assetsFromExcludedAlbums.addAll(assets);
|
assetsFromExcludedAlbums.addAll(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,11 +361,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final bool isEnabled = await _backgroundService.isBackgroundBackupEnabled();
|
final bool isEnabled = await _backgroundService.isBackgroundBackupEnabled();
|
||||||
state = state.copyWith(backgroundBackup: isEnabled);
|
state = state.copyWith(backgroundBackup: isEnabled);
|
||||||
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
||||||
await Future.wait([
|
await _getBackupAlbumsInfo();
|
||||||
_getBackupAlbumsInfo(),
|
await _updateServerInfo();
|
||||||
_updateServerInfo(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await _updateBackupAssetCount();
|
await _updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,9 @@ class BackupService {
|
|||||||
for (int i = 0; i < albums.length; i++) {
|
for (int i = 0; i < albums.length; i++) {
|
||||||
final AssetPathEntity? a = albums[i];
|
final AssetPathEntity? a = albums[i];
|
||||||
if (a != null && a.lastModified?.isBefore(lastBackup[i]) != true) {
|
if (a != null && a.lastModified?.isBefore(lastBackup[i]) != true) {
|
||||||
result.addAll(await a.getAssetListRange(start: 0, end: a.assetCount));
|
result.addAll(
|
||||||
|
await a.getAssetListRange(start: 0, end: await a.assetCountAsync),
|
||||||
|
);
|
||||||
lastBackup[i] = now;
|
lastBackup[i] = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -205,15 +203,23 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2.0),
|
padding: const EdgeInsets.only(top: 2.0),
|
||||||
child: Text(
|
child: FutureBuilder(
|
||||||
albumInfo.assetCount.toString() +
|
builder: ((context, snapshot) {
|
||||||
(albumInfo.isAll
|
if (snapshot.hasData) {
|
||||||
? " (${'backup_all'.tr()})"
|
return Text(
|
||||||
: ""),
|
snapshot.data.toString() +
|
||||||
style: TextStyle(
|
(albumInfo.isAll
|
||||||
fontSize: 12,
|
? " (${'backup_all'.tr()})"
|
||||||
color: Colors.grey[600],
|
: ""),
|
||||||
),
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const Text("0");
|
||||||
|
}),
|
||||||
|
future: albumInfo.assetCount,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -173,19 +173,19 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
OutlinedButton(
|
||||||
onPressed: () => launchUrl(
|
onPressed: () => launchUrl(
|
||||||
Uri.parse('https://dontkillmyapp.com'),
|
Uri.parse('https://dontkillmyapp.com'),
|
||||||
mode: LaunchMode.externalApplication),
|
mode: LaunchMode.externalApplication,
|
||||||
child: Text(
|
),
|
||||||
|
child: const Text(
|
||||||
"backup_controller_page_background_battery_info_link",
|
"backup_controller_page_background_battery_info_link",
|
||||||
style: TextStyle(color: buttonTextColor),
|
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
TextButton(
|
ElevatedButton(
|
||||||
child: Text(
|
child: const Text(
|
||||||
'backup_controller_page_background_battery_info_ok',
|
'backup_controller_page_background_battery_info_ok',
|
||||||
style: TextStyle(color: buttonTextColor),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
).tr(),
|
).tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@@ -636,8 +636,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
? ElevatedButton(
|
? ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: Colors.red[300],
|
foregroundColor: Colors.grey[50],
|
||||||
onPrimary: Colors.grey[50],
|
backgroundColor: Colors.red[300],
|
||||||
// padding: const EdgeInsets.all(14),
|
// padding: const EdgeInsets.all(14),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
|
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -10,13 +9,11 @@ class ImageGrid extends ConsumerWidget {
|
|||||||
final List<AssetResponseDto> sortedAssetGroup;
|
final List<AssetResponseDto> sortedAssetGroup;
|
||||||
final int tilesPerRow;
|
final int tilesPerRow;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final BaseCacheManager? cacheManager;
|
|
||||||
|
|
||||||
ImageGrid({
|
ImageGrid({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.assetGroup,
|
required this.assetGroup,
|
||||||
required this.sortedAssetGroup,
|
required this.sortedAssetGroup,
|
||||||
this.cacheManager,
|
|
||||||
this.tilesPerRow = 4,
|
this.tilesPerRow = 4,
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@@ -39,7 +36,6 @@ class ImageGrid extends ConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ThumbnailImage(
|
ThumbnailImage(
|
||||||
cacheManager: cacheManager,
|
|
||||||
asset: assetGroup[index],
|
asset: assetGroup[index],
|
||||||
assetList: sortedAssetGroup,
|
assetList: sortedAssetGroup,
|
||||||
showStorageIndicator: showStorageIndicator,
|
showStorageIndicator: showStorageIndicator,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final BackUpState backupState = ref.watch(backupProvider);
|
final BackUpState backupState = ref.watch(backupProvider);
|
||||||
bool isEnableAutoBackup =
|
bool isEnableAutoBackup = backupState.backgroundBackup ||
|
||||||
ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
||||||
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
@@ -17,13 +15,11 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
final AssetResponseDto asset;
|
final AssetResponseDto asset;
|
||||||
final List<AssetResponseDto> assetList;
|
final List<AssetResponseDto> assetList;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final BaseCacheManager? cacheManager;
|
|
||||||
|
|
||||||
const ThumbnailImage({
|
const ThumbnailImage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.assetList,
|
required this.assetList,
|
||||||
this.cacheManager,
|
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -36,7 +32,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
|
|
||||||
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
Widget buildSelectionIcon(AssetResponseDto asset) {
|
||||||
if (selectedAsset.contains(asset)) {
|
if (selectedAsset.contains(asset)) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
@@ -52,7 +48,6 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
debugPrint("View ${asset.id}");
|
|
||||||
if (isMultiSelectEnable &&
|
if (isMultiSelectEnable &&
|
||||||
selectedAsset.contains(asset) &&
|
selectedAsset.contains(asset) &&
|
||||||
selectedAsset.length == 1) {
|
selectedAsset.length == 1) {
|
||||||
@@ -95,11 +90,12 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
: const Border(),
|
: const Border(),
|
||||||
),
|
),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
cacheKey: asset.id,
|
cacheKey: 'thumbnail-image-${asset.id}',
|
||||||
cacheManager: cacheManager,
|
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400,
|
memCacheHeight: 200,
|
||||||
|
maxWidthDiskCache: 200,
|
||||||
|
maxHeightDiskCache: 200,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
@@ -115,6 +111,8 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) {
|
||||||
debugPrint("Error getting thumbnail $url = $error");
|
debugPrint("Error getting thumbnail $url = $error");
|
||||||
|
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
|
||||||
|
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.image_not_supported_outlined,
|
Icons.image_not_supported_outlined,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
@@ -127,7 +125,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.all(3.0),
|
padding: const EdgeInsets.all(3.0),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: _buildSelectionIcon(asset),
|
child: buildSelectionIcon(asset),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showStorageIndicator)
|
if (showStorageIndicator)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import 'package:immich_mobile/modules/settings/services/app_settings.service.dar
|
|||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.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/services/cache.service.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class HomePage extends HookConsumerWidget {
|
class HomePage extends HookConsumerWidget {
|
||||||
@@ -25,7 +24,6 @@ class HomePage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
final cacheService = ref.watch(cacheServiceProvider);
|
|
||||||
|
|
||||||
ScrollController scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
||||||
@@ -91,7 +89,6 @@ class HomePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
imageGridGroup.add(
|
imageGridGroup.add(
|
||||||
ImageGrid(
|
ImageGrid(
|
||||||
cacheManager: cacheService.getCache(CacheType.thumbnail),
|
|
||||||
assetGroup: immichAssetList,
|
assetGroup: immichAssetList,
|
||||||
sortedAssetGroup: sortedAssetList,
|
sortedAssetGroup: sortedAssetList,
|
||||||
tilesPerRow:
|
tilesPerRow:
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ class CacheSettings extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final CacheService cacheService = ref.watch(cacheServiceProvider);
|
final CacheService cacheService = ref.watch(cacheServiceProvider);
|
||||||
|
|
||||||
final clearCacheState = useState(false);
|
final clearCacheState = useState(false);
|
||||||
|
|
||||||
Future<void> clearCache() async {
|
Future<void> clearCache() async {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
|
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/cache_settings/cache_settings.dart';
|
|
||||||
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
|
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
|
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
|
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
|
||||||
@@ -42,7 +41,6 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
const ImageViewerQualitySetting(),
|
const ImageViewerQualitySetting(),
|
||||||
const ThemeSetting(),
|
const ThemeSetting(),
|
||||||
const AssetListSettings(),
|
const AssetListSettings(),
|
||||||
const CacheSettings(),
|
|
||||||
if (Platform.isAndroid) const NotificationSetting(),
|
if (Platform.isAndroid) const NotificationSetting(),
|
||||||
],
|
],
|
||||||
).toList(),
|
).toList(),
|
||||||
|
|||||||
@@ -59,8 +59,6 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
authToken: args.authToken,
|
authToken: args.authToken,
|
||||||
isZoomedFunction: args.isZoomedFunction,
|
isZoomedFunction: args.isZoomedFunction,
|
||||||
isZoomedListener: args.isZoomedListener,
|
isZoomedListener: args.isZoomedListener,
|
||||||
onLoadingCompleted: args.onLoadingCompleted,
|
|
||||||
onLoadingStart: args.onLoadingStart,
|
|
||||||
threeStageLoading: args.threeStageLoading));
|
threeStageLoading: args.threeStageLoading));
|
||||||
},
|
},
|
||||||
VideoViewerRoute.name: (routeData) {
|
VideoViewerRoute.name: (routeData) {
|
||||||
@@ -297,8 +295,6 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
|
|||||||
required String authToken,
|
required String authToken,
|
||||||
required void Function() isZoomedFunction,
|
required void Function() isZoomedFunction,
|
||||||
required ValueNotifier<bool> isZoomedListener,
|
required ValueNotifier<bool> isZoomedListener,
|
||||||
required void Function() onLoadingCompleted,
|
|
||||||
required void Function() onLoadingStart,
|
|
||||||
required bool threeStageLoading})
|
required bool threeStageLoading})
|
||||||
: super(ImageViewerRoute.name,
|
: super(ImageViewerRoute.name,
|
||||||
path: '/image-viewer-page',
|
path: '/image-viewer-page',
|
||||||
@@ -309,8 +305,6 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
|
|||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
isZoomedFunction: isZoomedFunction,
|
isZoomedFunction: isZoomedFunction,
|
||||||
isZoomedListener: isZoomedListener,
|
isZoomedListener: isZoomedListener,
|
||||||
onLoadingCompleted: onLoadingCompleted,
|
|
||||||
onLoadingStart: onLoadingStart,
|
|
||||||
threeStageLoading: threeStageLoading));
|
threeStageLoading: threeStageLoading));
|
||||||
|
|
||||||
static const String name = 'ImageViewerRoute';
|
static const String name = 'ImageViewerRoute';
|
||||||
@@ -324,8 +318,6 @@ class ImageViewerRouteArgs {
|
|||||||
required this.authToken,
|
required this.authToken,
|
||||||
required this.isZoomedFunction,
|
required this.isZoomedFunction,
|
||||||
required this.isZoomedListener,
|
required this.isZoomedListener,
|
||||||
required this.onLoadingCompleted,
|
|
||||||
required this.onLoadingStart,
|
|
||||||
required this.threeStageLoading});
|
required this.threeStageLoading});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
@@ -340,15 +332,11 @@ class ImageViewerRouteArgs {
|
|||||||
|
|
||||||
final ValueNotifier<bool> isZoomedListener;
|
final ValueNotifier<bool> isZoomedListener;
|
||||||
|
|
||||||
final void Function() onLoadingCompleted;
|
|
||||||
|
|
||||||
final void Function() onLoadingStart;
|
|
||||||
|
|
||||||
final bool threeStageLoading;
|
final bool threeStageLoading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, onLoadingCompleted: $onLoadingCompleted, onLoadingStart: $onLoadingStart, threeStageLoading: $threeStageLoading}';
|
return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, threeStageLoading: $threeStageLoading}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:immich_mobile/utils/immich_cache_info_repository.dart';
|
|||||||
enum CacheType {
|
enum CacheType {
|
||||||
// Shared cache for asset thumbnails in various modules
|
// Shared cache for asset thumbnails in various modules
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
|
||||||
imageViewerPreview,
|
imageViewerPreview,
|
||||||
imageViewerFull,
|
imageViewerFull,
|
||||||
albumThumbnail,
|
albumThumbnail,
|
||||||
@@ -67,7 +66,10 @@ class CacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BaseCacheManager _getDefaultCache(
|
BaseCacheManager _getDefaultCache(
|
||||||
String cacheName, int size, CacheInfoRepository repo) {
|
String cacheName,
|
||||||
|
int size,
|
||||||
|
CacheInfoRepository repo,
|
||||||
|
) {
|
||||||
return CacheManager(
|
return CacheManager(
|
||||||
Config(
|
Config(
|
||||||
cacheName,
|
cacheName,
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ doc/AddAssetsDto.md
|
|||||||
doc/AddUsersDto.md
|
doc/AddUsersDto.md
|
||||||
doc/AdminSignupResponseDto.md
|
doc/AdminSignupResponseDto.md
|
||||||
doc/AlbumApi.md
|
doc/AlbumApi.md
|
||||||
|
doc/AlbumCountResponseDto.md
|
||||||
doc/AlbumResponseDto.md
|
doc/AlbumResponseDto.md
|
||||||
doc/AssetApi.md
|
doc/AssetApi.md
|
||||||
doc/AssetCountByTimeBucket.md
|
doc/AssetCountByTimeBucket.md
|
||||||
doc/AssetCountByTimeBucketResponseDto.md
|
doc/AssetCountByTimeBucketResponseDto.md
|
||||||
|
doc/AssetCountByUserIdResponseDto.md
|
||||||
doc/AssetFileUploadResponseDto.md
|
doc/AssetFileUploadResponseDto.md
|
||||||
doc/AssetResponseDto.md
|
doc/AssetResponseDto.md
|
||||||
doc/AssetTypeEnum.md
|
doc/AssetTypeEnum.md
|
||||||
@@ -70,9 +72,11 @@ lib/auth/oauth.dart
|
|||||||
lib/model/add_assets_dto.dart
|
lib/model/add_assets_dto.dart
|
||||||
lib/model/add_users_dto.dart
|
lib/model/add_users_dto.dart
|
||||||
lib/model/admin_signup_response_dto.dart
|
lib/model/admin_signup_response_dto.dart
|
||||||
|
lib/model/album_count_response_dto.dart
|
||||||
lib/model/album_response_dto.dart
|
lib/model/album_response_dto.dart
|
||||||
lib/model/asset_count_by_time_bucket.dart
|
lib/model/asset_count_by_time_bucket.dart
|
||||||
lib/model/asset_count_by_time_bucket_response_dto.dart
|
lib/model/asset_count_by_time_bucket_response_dto.dart
|
||||||
|
lib/model/asset_count_by_user_id_response_dto.dart
|
||||||
lib/model/asset_file_upload_response_dto.dart
|
lib/model/asset_file_upload_response_dto.dart
|
||||||
lib/model/asset_response_dto.dart
|
lib/model/asset_response_dto.dart
|
||||||
lib/model/asset_type_enum.dart
|
lib/model/asset_type_enum.dart
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ Class | Method | HTTP request | Description
|
|||||||
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
|
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
|
||||||
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album |
|
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album |
|
||||||
*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
|
*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
|
||||||
|
*AlbumApi* | [**getAlbumCountByUserId**](doc//AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
|
||||||
*AlbumApi* | [**getAlbumInfo**](doc//AlbumApi.md#getalbuminfo) | **GET** /album/{albumId} |
|
*AlbumApi* | [**getAlbumInfo**](doc//AlbumApi.md#getalbuminfo) | **GET** /album/{albumId} |
|
||||||
*AlbumApi* | [**getAllAlbums**](doc//AlbumApi.md#getallalbums) | **GET** /album |
|
*AlbumApi* | [**getAllAlbums**](doc//AlbumApi.md#getallalbums) | **GET** /album |
|
||||||
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
|
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
|
||||||
@@ -81,6 +82,7 @@ Class | Method | HTTP request | Description
|
|||||||
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
|
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
|
||||||
*AssetApi* | [**getAssetByTimeBucket**](doc//AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket |
|
*AssetApi* | [**getAssetByTimeBucket**](doc//AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket |
|
||||||
*AssetApi* | [**getAssetCountByTimeBucket**](doc//AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket |
|
*AssetApi* | [**getAssetCountByTimeBucket**](doc//AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket |
|
||||||
|
*AssetApi* | [**getAssetCountByUserId**](doc//AssetApi.md#getassetcountbyuserid) | **GET** /asset/count-by-user-id |
|
||||||
*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
|
*AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
|
||||||
*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
|
*AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
|
||||||
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
||||||
@@ -113,9 +115,11 @@ Class | Method | HTTP request | Description
|
|||||||
- [AddAssetsDto](doc//AddAssetsDto.md)
|
- [AddAssetsDto](doc//AddAssetsDto.md)
|
||||||
- [AddUsersDto](doc//AddUsersDto.md)
|
- [AddUsersDto](doc//AddUsersDto.md)
|
||||||
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
|
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
|
||||||
|
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
|
||||||
- [AlbumResponseDto](doc//AlbumResponseDto.md)
|
- [AlbumResponseDto](doc//AlbumResponseDto.md)
|
||||||
- [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
|
- [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
|
||||||
- [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
|
- [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
|
||||||
|
- [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md)
|
||||||
- [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
|
- [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
|
||||||
- [AssetResponseDto](doc//AssetResponseDto.md)
|
- [AssetResponseDto](doc//AssetResponseDto.md)
|
||||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Method | HTTP request | Description
|
|||||||
[**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
|
[**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
|
||||||
[**createAlbum**](AlbumApi.md#createalbum) | **POST** /album |
|
[**createAlbum**](AlbumApi.md#createalbum) | **POST** /album |
|
||||||
[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
|
[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
|
||||||
|
[**getAlbumCountByUserId**](AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
|
||||||
[**getAlbumInfo**](AlbumApi.md#getalbuminfo) | **GET** /album/{albumId} |
|
[**getAlbumInfo**](AlbumApi.md#getalbuminfo) | **GET** /album/{albumId} |
|
||||||
[**getAllAlbums**](AlbumApi.md#getallalbums) | **GET** /album |
|
[**getAllAlbums**](AlbumApi.md#getallalbums) | **GET** /album |
|
||||||
[**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
|
[**removeAssetFromAlbum**](AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
|
||||||
@@ -211,6 +212,49 @@ void (empty response body)
|
|||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **getAlbumCountByUserId**
|
||||||
|
> AlbumCountResponseDto getAlbumCountByUserId()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = AlbumApi();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.getAlbumCountByUserId();
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AlbumApi->getAlbumCountByUserId: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
This endpoint does not need any parameter.
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**AlbumCountResponseDto**](AlbumCountResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **getAlbumInfo**
|
# **getAlbumInfo**
|
||||||
> AlbumResponseDto getAlbumInfo(albumId)
|
> AlbumResponseDto getAlbumInfo(albumId)
|
||||||
|
|
||||||
|
|||||||
17
mobile/openapi/doc/AlbumCountResponseDto.md
Normal file
17
mobile/openapi/doc/AlbumCountResponseDto.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# openapi.model.AlbumCountResponseDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**owned** | **int** | |
|
||||||
|
**shared** | **int** | |
|
||||||
|
**sharing** | **int** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ Method | HTTP request | Description
|
|||||||
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
|
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
|
||||||
[**getAssetByTimeBucket**](AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket |
|
[**getAssetByTimeBucket**](AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket |
|
||||||
[**getAssetCountByTimeBucket**](AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket |
|
[**getAssetCountByTimeBucket**](AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket |
|
||||||
|
[**getAssetCountByUserId**](AssetApi.md#getassetcountbyuserid) | **GET** /asset/count-by-user-id |
|
||||||
[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
|
[**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms |
|
||||||
[**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
|
[**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} |
|
||||||
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
||||||
@@ -363,6 +364,49 @@ Name | Type | Description | Notes
|
|||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **getAssetCountByUserId**
|
||||||
|
> AssetCountByUserIdResponseDto getAssetCountByUserId()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = AssetApi();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.getAssetCountByUserId();
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AssetApi->getAssetCountByUserId: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
This endpoint does not need any parameter.
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**AssetCountByUserIdResponseDto**](AssetCountByUserIdResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **getAssetSearchTerms**
|
# **getAssetSearchTerms**
|
||||||
> List<String> getAssetSearchTerms()
|
> List<String> getAssetSearchTerms()
|
||||||
|
|
||||||
|
|||||||
16
mobile/openapi/doc/AssetCountByUserIdResponseDto.md
Normal file
16
mobile/openapi/doc/AssetCountByUserIdResponseDto.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# openapi.model.AssetCountByUserIdResponseDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**photos** | **int** | |
|
||||||
|
**videos** | **int** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
@@ -37,9 +37,11 @@ part 'api/user_api.dart';
|
|||||||
part 'model/add_assets_dto.dart';
|
part 'model/add_assets_dto.dart';
|
||||||
part 'model/add_users_dto.dart';
|
part 'model/add_users_dto.dart';
|
||||||
part 'model/admin_signup_response_dto.dart';
|
part 'model/admin_signup_response_dto.dart';
|
||||||
|
part 'model/album_count_response_dto.dart';
|
||||||
part 'model/album_response_dto.dart';
|
part 'model/album_response_dto.dart';
|
||||||
part 'model/asset_count_by_time_bucket.dart';
|
part 'model/asset_count_by_time_bucket.dart';
|
||||||
part 'model/asset_count_by_time_bucket_response_dto.dart';
|
part 'model/asset_count_by_time_bucket_response_dto.dart';
|
||||||
|
part 'model/asset_count_by_user_id_response_dto.dart';
|
||||||
part 'model/asset_file_upload_response_dto.dart';
|
part 'model/asset_file_upload_response_dto.dart';
|
||||||
part 'model/asset_response_dto.dart';
|
part 'model/asset_response_dto.dart';
|
||||||
part 'model/asset_type_enum.dart';
|
part 'model/asset_type_enum.dart';
|
||||||
|
|||||||
@@ -207,6 +207,47 @@ class AlbumApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /album/count-by-user-id' operation and returns the [Response].
|
||||||
|
Future<Response> getAlbumCountByUserIdWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/album/count-by-user-id';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AlbumCountResponseDto?> getAlbumCountByUserId() async {
|
||||||
|
final response = await getAlbumCountByUserIdWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AlbumCountResponseDto',) as AlbumCountResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /album/{albumId}' operation and returns the [Response].
|
/// Performs an HTTP 'GET /album/{albumId}' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -395,6 +395,47 @@ class AssetApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /asset/count-by-user-id' operation and returns the [Response].
|
||||||
|
Future<Response> getAssetCountByUserIdWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/asset/count-by-user-id';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetCountByUserIdResponseDto?> getAssetCountByUserId() async {
|
||||||
|
final response = await getAssetCountByUserIdWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetCountByUserIdResponseDto',) as AssetCountByUserIdResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response].
|
/// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response].
|
||||||
Future<Response> getAssetSearchTermsWithHttpInfo() async {
|
Future<Response> getAssetSearchTermsWithHttpInfo() async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
|
|||||||
@@ -198,12 +198,16 @@ class ApiClient {
|
|||||||
return AddUsersDto.fromJson(value);
|
return AddUsersDto.fromJson(value);
|
||||||
case 'AdminSignupResponseDto':
|
case 'AdminSignupResponseDto':
|
||||||
return AdminSignupResponseDto.fromJson(value);
|
return AdminSignupResponseDto.fromJson(value);
|
||||||
|
case 'AlbumCountResponseDto':
|
||||||
|
return AlbumCountResponseDto.fromJson(value);
|
||||||
case 'AlbumResponseDto':
|
case 'AlbumResponseDto':
|
||||||
return AlbumResponseDto.fromJson(value);
|
return AlbumResponseDto.fromJson(value);
|
||||||
case 'AssetCountByTimeBucket':
|
case 'AssetCountByTimeBucket':
|
||||||
return AssetCountByTimeBucket.fromJson(value);
|
return AssetCountByTimeBucket.fromJson(value);
|
||||||
case 'AssetCountByTimeBucketResponseDto':
|
case 'AssetCountByTimeBucketResponseDto':
|
||||||
return AssetCountByTimeBucketResponseDto.fromJson(value);
|
return AssetCountByTimeBucketResponseDto.fromJson(value);
|
||||||
|
case 'AssetCountByUserIdResponseDto':
|
||||||
|
return AssetCountByUserIdResponseDto.fromJson(value);
|
||||||
case 'AssetFileUploadResponseDto':
|
case 'AssetFileUploadResponseDto':
|
||||||
return AssetFileUploadResponseDto.fromJson(value);
|
return AssetFileUploadResponseDto.fromJson(value);
|
||||||
case 'AssetResponseDto':
|
case 'AssetResponseDto':
|
||||||
|
|||||||
127
mobile/openapi/lib/model/album_count_response_dto.dart
Normal file
127
mobile/openapi/lib/model/album_count_response_dto.dart
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class AlbumCountResponseDto {
|
||||||
|
/// Returns a new [AlbumCountResponseDto] instance.
|
||||||
|
AlbumCountResponseDto({
|
||||||
|
required this.owned,
|
||||||
|
required this.shared,
|
||||||
|
required this.sharing,
|
||||||
|
});
|
||||||
|
|
||||||
|
int owned;
|
||||||
|
|
||||||
|
int shared;
|
||||||
|
|
||||||
|
int sharing;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is AlbumCountResponseDto &&
|
||||||
|
other.owned == owned &&
|
||||||
|
other.shared == shared &&
|
||||||
|
other.sharing == sharing;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(owned.hashCode) +
|
||||||
|
(shared.hashCode) +
|
||||||
|
(sharing.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AlbumCountResponseDto[owned=$owned, shared=$shared, sharing=$sharing]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_json[r'owned'] = owned;
|
||||||
|
_json[r'shared'] = shared;
|
||||||
|
_json[r'sharing'] = sharing;
|
||||||
|
return _json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [AlbumCountResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static AlbumCountResponseDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "AlbumCountResponseDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "AlbumCountResponseDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return AlbumCountResponseDto(
|
||||||
|
owned: mapValueOfType<int>(json, r'owned')!,
|
||||||
|
shared: mapValueOfType<int>(json, r'shared')!,
|
||||||
|
sharing: mapValueOfType<int>(json, r'sharing')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<AlbumCountResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <AlbumCountResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = AlbumCountResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, AlbumCountResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, AlbumCountResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = AlbumCountResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of AlbumCountResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<AlbumCountResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<AlbumCountResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = AlbumCountResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'owned',
|
||||||
|
'shared',
|
||||||
|
'sharing',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class AssetCountByUserIdResponseDto {
|
||||||
|
/// Returns a new [AssetCountByUserIdResponseDto] instance.
|
||||||
|
AssetCountByUserIdResponseDto({
|
||||||
|
required this.photos,
|
||||||
|
required this.videos,
|
||||||
|
});
|
||||||
|
|
||||||
|
int photos;
|
||||||
|
|
||||||
|
int videos;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is AssetCountByUserIdResponseDto &&
|
||||||
|
other.photos == photos &&
|
||||||
|
other.videos == videos;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(photos.hashCode) +
|
||||||
|
(videos.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AssetCountByUserIdResponseDto[photos=$photos, videos=$videos]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_json[r'photos'] = photos;
|
||||||
|
_json[r'videos'] = videos;
|
||||||
|
return _json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [AssetCountByUserIdResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static AssetCountByUserIdResponseDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
// Ensure that the map contains the required keys.
|
||||||
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
|
// Note 2: this code is stripped in release mode!
|
||||||
|
assert(() {
|
||||||
|
requiredKeys.forEach((key) {
|
||||||
|
assert(json.containsKey(key), 'Required key "AssetCountByUserIdResponseDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "AssetCountByUserIdResponseDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return AssetCountByUserIdResponseDto(
|
||||||
|
photos: mapValueOfType<int>(json, r'photos')!,
|
||||||
|
videos: mapValueOfType<int>(json, r'videos')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<AssetCountByUserIdResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <AssetCountByUserIdResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = AssetCountByUserIdResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, AssetCountByUserIdResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, AssetCountByUserIdResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = AssetCountByUserIdResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of AssetCountByUserIdResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<AssetCountByUserIdResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<AssetCountByUserIdResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = AssetCountByUserIdResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'photos',
|
||||||
|
'videos',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -76,69 +76,72 @@ class AssetResponseDto {
|
|||||||
SmartInfoResponseDto? smartInfo;
|
SmartInfoResponseDto? smartInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
bool operator ==(Object other) =>
|
||||||
other.type == type &&
|
identical(this, other) ||
|
||||||
other.id == id &&
|
other is AssetResponseDto &&
|
||||||
other.deviceAssetId == deviceAssetId &&
|
other.type == type &&
|
||||||
other.ownerId == ownerId &&
|
other.id == id &&
|
||||||
other.deviceId == deviceId &&
|
other.deviceAssetId == deviceAssetId &&
|
||||||
other.originalPath == originalPath &&
|
other.ownerId == ownerId &&
|
||||||
other.resizePath == resizePath &&
|
other.deviceId == deviceId &&
|
||||||
other.createdAt == createdAt &&
|
other.originalPath == originalPath &&
|
||||||
other.modifiedAt == modifiedAt &&
|
other.resizePath == resizePath &&
|
||||||
other.isFavorite == isFavorite &&
|
other.createdAt == createdAt &&
|
||||||
other.mimeType == mimeType &&
|
other.modifiedAt == modifiedAt &&
|
||||||
other.duration == duration &&
|
other.isFavorite == isFavorite &&
|
||||||
other.webpPath == webpPath &&
|
other.mimeType == mimeType &&
|
||||||
other.encodedVideoPath == encodedVideoPath &&
|
other.duration == duration &&
|
||||||
other.exifInfo == exifInfo &&
|
other.webpPath == webpPath &&
|
||||||
other.smartInfo == smartInfo;
|
other.encodedVideoPath == encodedVideoPath &&
|
||||||
|
other.exifInfo == exifInfo &&
|
||||||
|
other.smartInfo == smartInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(type.hashCode) +
|
(type.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(deviceAssetId.hashCode) +
|
(deviceAssetId.hashCode) +
|
||||||
(ownerId.hashCode) +
|
(ownerId.hashCode) +
|
||||||
(deviceId.hashCode) +
|
(deviceId.hashCode) +
|
||||||
(originalPath.hashCode) +
|
(originalPath.hashCode) +
|
||||||
(resizePath == null ? 0 : resizePath!.hashCode) +
|
(resizePath == null ? 0 : resizePath!.hashCode) +
|
||||||
(createdAt.hashCode) +
|
(createdAt.hashCode) +
|
||||||
(modifiedAt.hashCode) +
|
(modifiedAt.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
(isFavorite.hashCode) +
|
||||||
(mimeType == null ? 0 : mimeType!.hashCode) +
|
(mimeType == null ? 0 : mimeType!.hashCode) +
|
||||||
(duration.hashCode) +
|
(duration.hashCode) +
|
||||||
(webpPath == null ? 0 : webpPath!.hashCode) +
|
(webpPath == null ? 0 : webpPath!.hashCode) +
|
||||||
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
||||||
(exifInfo == null ? 0 : exifInfo!.hashCode) +
|
(exifInfo == null ? 0 : exifInfo!.hashCode) +
|
||||||
(smartInfo == null ? 0 : smartInfo!.hashCode);
|
(smartInfo == null ? 0 : smartInfo!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
|
String toString() =>
|
||||||
|
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final _json = <String, dynamic>{};
|
final _json = <String, dynamic>{};
|
||||||
_json[r'type'] = type;
|
_json[r'type'] = type;
|
||||||
_json[r'id'] = id;
|
_json[r'id'] = id;
|
||||||
_json[r'deviceAssetId'] = deviceAssetId;
|
_json[r'deviceAssetId'] = deviceAssetId;
|
||||||
_json[r'ownerId'] = ownerId;
|
_json[r'ownerId'] = ownerId;
|
||||||
_json[r'deviceId'] = deviceId;
|
_json[r'deviceId'] = deviceId;
|
||||||
_json[r'originalPath'] = originalPath;
|
_json[r'originalPath'] = originalPath;
|
||||||
if (resizePath != null) {
|
if (resizePath != null) {
|
||||||
_json[r'resizePath'] = resizePath;
|
_json[r'resizePath'] = resizePath;
|
||||||
} else {
|
} else {
|
||||||
_json[r'resizePath'] = null;
|
_json[r'resizePath'] = null;
|
||||||
}
|
}
|
||||||
_json[r'createdAt'] = createdAt;
|
_json[r'createdAt'] = createdAt;
|
||||||
_json[r'modifiedAt'] = modifiedAt;
|
_json[r'modifiedAt'] = modifiedAt;
|
||||||
_json[r'isFavorite'] = isFavorite;
|
_json[r'isFavorite'] = isFavorite;
|
||||||
if (mimeType != null) {
|
if (mimeType != null) {
|
||||||
_json[r'mimeType'] = mimeType;
|
_json[r'mimeType'] = mimeType;
|
||||||
} else {
|
} else {
|
||||||
_json[r'mimeType'] = null;
|
_json[r'mimeType'] = null;
|
||||||
}
|
}
|
||||||
_json[r'duration'] = duration;
|
_json[r'duration'] = duration;
|
||||||
if (webpPath != null) {
|
if (webpPath != null) {
|
||||||
_json[r'webpPath'] = webpPath;
|
_json[r'webpPath'] = webpPath;
|
||||||
} else {
|
} else {
|
||||||
@@ -172,13 +175,13 @@ class AssetResponseDto {
|
|||||||
// Ensure that the map contains the required keys.
|
// Ensure that the map contains the required keys.
|
||||||
// Note 1: the values aren't checked for validity beyond being non-null.
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
// Note 2: this code is stripped in release mode!
|
// Note 2: this code is stripped in release mode!
|
||||||
assert(() {
|
// assert(() {
|
||||||
requiredKeys.forEach((key) {
|
// requiredKeys.forEach((key) {
|
||||||
assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
|
// assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
|
||||||
assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
|
// assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
|
||||||
});
|
// });
|
||||||
return true;
|
// return true;
|
||||||
}());
|
// }());
|
||||||
|
|
||||||
return AssetResponseDto(
|
return AssetResponseDto(
|
||||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||||
@@ -202,7 +205,10 @@ class AssetResponseDto {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
static List<AssetResponseDto>? listFromJson(
|
||||||
|
dynamic json, {
|
||||||
|
bool growable = false,
|
||||||
|
}) {
|
||||||
final result = <AssetResponseDto>[];
|
final result = <AssetResponseDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
@@ -230,12 +236,18 @@ class AssetResponseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
|
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
|
||||||
static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
static Map<String, List<AssetResponseDto>> mapListFromJson(
|
||||||
|
dynamic json, {
|
||||||
|
bool growable = false,
|
||||||
|
}) {
|
||||||
final map = <String, List<AssetResponseDto>>{};
|
final map = <String, List<AssetResponseDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
|
final value = AssetResponseDto.listFromJson(
|
||||||
|
entry.value,
|
||||||
|
growable: growable,
|
||||||
|
);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@@ -262,4 +274,3 @@ class AssetResponseDto {
|
|||||||
'encodedVideoPath',
|
'encodedVideoPath',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
mobile/openapi/test/album_count_response_dto_test.dart
Normal file
37
mobile/openapi/test/album_count_response_dto_test.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for AlbumCountResponseDto
|
||||||
|
void main() {
|
||||||
|
// final instance = AlbumCountResponseDto();
|
||||||
|
|
||||||
|
group('test AlbumCountResponseDto', () {
|
||||||
|
// int owned
|
||||||
|
test('to test the property `owned`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// int shared
|
||||||
|
test('to test the property `shared`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// int sharing
|
||||||
|
test('to test the property `sharing`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for AssetCountByUserIdResponseDto
|
||||||
|
void main() {
|
||||||
|
// final instance = AssetCountByUserIdResponseDto();
|
||||||
|
|
||||||
|
group('test AssetCountByUserIdResponseDto', () {
|
||||||
|
// int photos
|
||||||
|
test('to test the property `photos`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// int videos
|
||||||
|
test('to test the property `videos`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,14 +7,14 @@ packages:
|
|||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "38.0.0"
|
version: "47.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.4.1"
|
version: "4.7.0"
|
||||||
archive:
|
archive:
|
||||||
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: "4.0.1"
|
version: "5.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: "4.0.0"
|
version: "5.0.2"
|
||||||
badges:
|
badges:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -77,7 +77,7 @@ packages:
|
|||||||
name: build_config
|
name: build_config
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.1.0"
|
||||||
build_daemon:
|
build_daemon:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -98,7 +98,7 @@ packages:
|
|||||||
name: build_runner
|
name: build_runner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.10"
|
version: "2.2.1"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -126,21 +126,21 @@ packages:
|
|||||||
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.1"
|
version: "3.2.2"
|
||||||
cached_network_image_platform_interface:
|
cached_network_image_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cached_network_image_platform_interface
|
name: cached_network_image_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "2.0.0"
|
||||||
cached_network_image_web:
|
cached_network_image_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cached_network_image_web
|
name: cached_network_image_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.2"
|
||||||
cancellation_token:
|
cancellation_token:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -162,13 +162,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.1"
|
||||||
charcode:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: charcode
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.1"
|
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -182,7 +175,7 @@ packages:
|
|||||||
name: chewie
|
name: chewie
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.5"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -238,7 +231,7 @@ packages:
|
|||||||
name: cupertino_icons
|
name: cupertino_icons
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.5"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -322,7 +315,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.8"
|
version: "0.6.8"
|
||||||
flutter_cache_manager:
|
flutter_cache_manager:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_cache_manager
|
name: flutter_cache_manager
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
@@ -461,7 +454,7 @@ packages:
|
|||||||
name: hive_generator
|
name: hive_generator
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.3"
|
||||||
hooks_riverpod:
|
hooks_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -503,7 +496,7 @@ packages:
|
|||||||
name: image
|
name: image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.2.0"
|
||||||
image_picker:
|
image_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -783,7 +776,7 @@ packages:
|
|||||||
name: photo_manager
|
name: photo_manager
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0+2"
|
version: "2.2.1"
|
||||||
photo_view:
|
photo_view:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -839,7 +832,7 @@ packages:
|
|||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.2"
|
version: "6.0.3"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1139,20 +1132,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "0.3.1"
|
||||||
universal_html:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: universal_html
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.8"
|
|
||||||
universal_io:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: universal_io
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.4"
|
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1223,27 +1202,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
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.4.2"
|
version: "2.4.7"
|
||||||
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.3.3"
|
version: "2.3.9"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1271,7 +1243,7 @@ packages:
|
|||||||
name: wakelock
|
name: wakelock
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1+2"
|
version: "0.6.2"
|
||||||
wakelock_macos:
|
wakelock_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1341,7 +1313,7 @@ packages:
|
|||||||
name: xml
|
name: xml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.4.1"
|
version: "6.1.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1350,5 +1322,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.0 <3.0.0"
|
dart: ">=2.18.0 <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.3.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.27.0+37
|
version: 1.29.1+43
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
@@ -11,22 +11,22 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
photo_manager: ^2.0.6
|
photo_manager: ^2.2.1
|
||||||
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: ^2.2.1
|
hive: ^2.2.1
|
||||||
hive_flutter: ^1.1.0
|
hive_flutter: ^1.1.0
|
||||||
dio: ^4.0.4
|
dio: ^4.0.4
|
||||||
cached_network_image: ^3.2.1
|
cached_network_image: ^3.2.2
|
||||||
percent_indicator: ^4.2.2
|
percent_indicator: ^4.2.2
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
auto_route: ^4.0.1
|
auto_route: ^5.0.1
|
||||||
exif: ^3.1.1
|
exif: ^3.1.1
|
||||||
transparent_image: ^2.0.0
|
transparent_image: ^2.0.0
|
||||||
flutter_launcher_icons: "^0.9.2"
|
flutter_launcher_icons: "^0.9.2"
|
||||||
fluttertoast: ^8.0.8
|
fluttertoast: ^8.0.8
|
||||||
video_player: ^2.2.18
|
video_player: ^2.2.18
|
||||||
chewie: ^1.2.2
|
chewie: ^1.3.5
|
||||||
badges: ^2.0.2
|
badges: ^2.0.2
|
||||||
photo_view: ^0.14.0
|
photo_view: ^0.14.0
|
||||||
socket_io_client: ^2.0.0-beta.4-nullsafety.0
|
socket_io_client: ^2.0.0-beta.4-nullsafety.0
|
||||||
@@ -43,7 +43,6 @@ dependencies:
|
|||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
share_plus: ^4.0.10
|
share_plus: ^4.0.10
|
||||||
flutter_displaymode: ^0.4.0
|
flutter_displaymode: ^0.4.0
|
||||||
flutter_cache_manager: 3.3.0
|
|
||||||
|
|
||||||
path: ^1.8.1
|
path: ^1.8.1
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.0.11
|
||||||
@@ -59,8 +58,8 @@ dev_dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^2.0.1
|
flutter_lints: ^2.0.1
|
||||||
hive_generator: ^1.1.2
|
hive_generator: ^1.1.2
|
||||||
build_runner: ^2.1.7
|
build_runner: ^2.2.1
|
||||||
auto_route_generator: ^4.0.0
|
auto_route_generator: ^5.0.2
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
FROM nginx:latest
|
FROM docker.io/nginxinc/nginx-unprivileged:latest
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf "/etc/nginx/nginx.conf"
|
||||||
|
|
||||||
EXPOSE 80
|
CMD nginx -g "daemon off;"
|
||||||
EXPOSE 443
|
|
||||||
142
nginx/nginx.conf
142
nginx/nginx.conf
@@ -1,73 +1,93 @@
|
|||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
|
||||||
default upgrade;
|
worker_processes auto;
|
||||||
'' close;
|
error_log /var/log/nginx/error.log;
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
|
||||||
|
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
|
||||||
|
include /usr/share/nginx/modules/*.conf;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
# events {
|
http {
|
||||||
# worker_connections 1000;
|
map $http_upgrade $connection_upgrade {
|
||||||
# }
|
default upgrade;
|
||||||
|
'' close;
|
||||||
server {
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_min_length 1000;
|
|
||||||
gunzip on;
|
|
||||||
|
|
||||||
client_max_body_size 50000M;
|
|
||||||
|
|
||||||
listen 80;
|
|
||||||
access_log off;
|
|
||||||
|
|
||||||
location /api {
|
|
||||||
|
|
||||||
# Compression
|
|
||||||
gzip_static on;
|
|
||||||
gzip_min_length 1000;
|
|
||||||
gzip_comp_level 2;
|
|
||||||
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_buffer_size 16k;
|
|
||||||
proxy_busy_buffers_size 24k;
|
|
||||||
proxy_buffers 64 4k;
|
|
||||||
proxy_force_ranges on;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
|
|
||||||
rewrite /api/(.*) /$1 break;
|
|
||||||
|
|
||||||
proxy_pass http://immich-server:3001;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
client_body_temp_path /tmp/client_temp;
|
||||||
|
proxy_temp_path /tmp/proxy_temp_path;
|
||||||
|
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||||
|
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||||
|
scgi_temp_path /tmp/scgi_temp;
|
||||||
|
|
||||||
# Compression
|
# events {
|
||||||
gzip_static on;
|
# worker_connections 1000;
|
||||||
|
# }
|
||||||
|
|
||||||
|
server {
|
||||||
|
|
||||||
|
gzip on;
|
||||||
gzip_min_length 1000;
|
gzip_min_length 1000;
|
||||||
gzip_comp_level 2;
|
gunzip on;
|
||||||
|
|
||||||
proxy_buffering off;
|
client_max_body_size 50000M;
|
||||||
proxy_buffer_size 16k;
|
|
||||||
proxy_busy_buffers_size 24k;
|
|
||||||
proxy_buffers 64 4k;
|
|
||||||
proxy_force_ranges on;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
listen 8080;
|
||||||
proxy_set_header Host $host;
|
access_log off;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
|
|
||||||
proxy_pass http://immich-web:3000;
|
location /api {
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
gzip_static on;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_comp_level 2;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_buffer_size 16k;
|
||||||
|
proxy_busy_buffers_size 24k;
|
||||||
|
proxy_buffers 64 4k;
|
||||||
|
proxy_force_ranges on;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
|
rewrite /api/(.*) /$1 break;
|
||||||
|
|
||||||
|
proxy_pass http://immich-server:3001;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
gzip_static on;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_comp_level 2;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_buffer_size 16k;
|
||||||
|
proxy_busy_buffers_size 24k;
|
||||||
|
proxy_buffers 64 4k;
|
||||||
|
proxy_force_ranges on;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
|
proxy_pass http://immich-web:3000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ import { CreateAlbumDto } from './dto/create-album.dto';
|
|||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
import { UpdateAlbumDto } from './dto/update-album.dto';
|
||||||
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||||
|
|
||||||
export interface IAlbumRepository {
|
export interface IAlbumRepository {
|
||||||
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
|
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
|
||||||
@@ -23,6 +23,7 @@ export interface IAlbumRepository {
|
|||||||
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>;
|
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>;
|
||||||
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
|
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
|
||||||
getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>;
|
getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||||
|
getCountByUserId(userId: string): Promise<AlbumCountResponseDto>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ALBUM_REPOSITORY = 'ALBUM_REPOSITORY';
|
export const ALBUM_REPOSITORY = 'ALBUM_REPOSITORY';
|
||||||
@@ -42,6 +43,23 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
private dataSource: DataSource,
|
private dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async getCountByUserId(userId: string): Promise<AlbumCountResponseDto> {
|
||||||
|
const ownedAlbums = await this.albumRepository.find({ where: { ownerId: userId }, relations: ['sharedUsers'] });
|
||||||
|
|
||||||
|
const sharedAlbums = await this.userAlbumRepository.count({
|
||||||
|
where: { sharedUserId: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
let sharedAlbumCount = 0;
|
||||||
|
ownedAlbums.map((album) => {
|
||||||
|
if (album.sharedUsers?.length) {
|
||||||
|
sharedAlbumCount += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new AlbumCountResponseDto(ownedAlbums.length, sharedAlbums, sharedAlbumCount);
|
||||||
|
}
|
||||||
|
|
||||||
async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> {
|
async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> {
|
||||||
return this.dataSource.transaction(async (transactionalEntityManager) => {
|
return this.dataSource.transaction(async (transactionalEntityManager) => {
|
||||||
// Create album entity
|
// Create album entity
|
||||||
@@ -151,32 +169,32 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]> {
|
async getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]> {
|
||||||
let query = this.albumRepository.createQueryBuilder('album');
|
const query = this.albumRepository.createQueryBuilder('album');
|
||||||
|
|
||||||
const albums = await query
|
const albums = await query
|
||||||
.where('album.ownerId = :ownerId', { ownerId: userId })
|
.where('album.ownerId = :ownerId', { ownerId: userId })
|
||||||
.andWhere((qb) => {
|
.andWhere((qb) => {
|
||||||
// shared with userId
|
// shared with userId
|
||||||
const subQuery = qb
|
const subQuery = qb
|
||||||
.subQuery()
|
.subQuery()
|
||||||
.select('assetAlbum.albumId')
|
.select('assetAlbum.albumId')
|
||||||
.from(AssetAlbumEntity, 'assetAlbum')
|
.from(AssetAlbumEntity, 'assetAlbum')
|
||||||
.where('assetAlbum.assetId = :assetId', {assetId: assetId})
|
.where('assetAlbum.assetId = :assetId', { assetId: assetId })
|
||||||
.getQuery();
|
.getQuery();
|
||||||
return `album.id IN ${subQuery}`;
|
return `album.id IN ${subQuery}`;
|
||||||
})
|
})
|
||||||
.leftJoinAndSelect('album.assets', 'assets')
|
.leftJoinAndSelect('album.assets', 'assets')
|
||||||
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||||
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
||||||
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
||||||
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
|
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return albums;
|
return albums;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(albumId: string): Promise<AlbumEntity | undefined> {
|
async get(albumId: string): Promise<AlbumEntity | undefined> {
|
||||||
let query = this.albumRepository.createQueryBuilder('album');
|
const query = this.albumRepository.createQueryBuilder('album');
|
||||||
|
|
||||||
const album = await query
|
const album = await query
|
||||||
.where('album.id = :albumId', { albumId })
|
.where('album.id = :albumId', { albumId })
|
||||||
@@ -228,9 +246,10 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
|
|
||||||
// TODO: No need to return boolean if using a singe delete query
|
// TODO: No need to return boolean if using a singe delete query
|
||||||
if (deleteAssetCount == removeAssetsDto.assetIds.length) {
|
if (deleteAssetCount == removeAssetsDto.assetIds.length) {
|
||||||
const retAlbum = await this.get(album.id) as AlbumEntity;
|
const retAlbum = (await this.get(album.id)) as AlbumEntity;
|
||||||
|
|
||||||
if (retAlbum?.assets?.length === 0) { // is empty album
|
if (retAlbum?.assets?.length === 0) {
|
||||||
|
// is empty album
|
||||||
await this.albumRepository.update(album.id, { albumThumbnailAssetId: null });
|
await this.albumRepository.update(album.id, { albumThumbnailAssetId: null });
|
||||||
retAlbum.albumThumbnailAssetId = null;
|
retAlbum.albumThumbnailAssetId = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { UpdateAlbumDto } from './dto/update-album.dto';
|
|||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
||||||
|
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||||
|
|
||||||
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
|
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@@ -33,6 +34,11 @@ import { AlbumResponseDto } from './response-dto/album-response.dto';
|
|||||||
export class AlbumController {
|
export class AlbumController {
|
||||||
constructor(private readonly albumService: AlbumService) {}
|
constructor(private readonly albumService: AlbumService) {}
|
||||||
|
|
||||||
|
@Get('count-by-user-id')
|
||||||
|
async getAlbumCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
|
||||||
|
return this.albumService.getAlbumCountByUserId(authUser);
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
|
async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
|
||||||
return this.albumService.create(authUser, createAlbumDto);
|
return this.albumService.create(authUser, createAlbumDto);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { AlbumEntity } from '../../../../../libs/database/src/entities/album.ent
|
|||||||
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
||||||
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
|
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
|
||||||
import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
|
import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
|
||||||
|
import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([AssetEntity, UserEntity, AlbumEntity, AssetAlbumEntity, UserAlbumEntity])],
|
imports: [TypeOrmModule.forFeature([AssetEntity, UserEntity, AlbumEntity, AssetAlbumEntity, UserAlbumEntity])],
|
||||||
@@ -18,6 +19,10 @@ import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
|
|||||||
provide: ALBUM_REPOSITORY,
|
provide: ALBUM_REPOSITORY,
|
||||||
useClass: AlbumRepository,
|
useClass: AlbumRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ASSET_REPOSITORY,
|
||||||
|
useClass: AssetRepository,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AlbumModule {}
|
export class AlbumModule {}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|||||||
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
import { AlbumEntity } from '@app/database/entities/album.entity';
|
import { AlbumEntity } from '@app/database/entities/album.entity';
|
||||||
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
||||||
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
import { IAssetRepository } from '../asset/asset-repository';
|
||||||
|
|
||||||
describe('Album service', () => {
|
describe('Album service', () => {
|
||||||
let sut: AlbumService;
|
let sut: AlbumService;
|
||||||
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
||||||
|
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
||||||
|
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
const authUser: AuthUserDto = Object.freeze({
|
||||||
id: '1111',
|
id: '1111',
|
||||||
email: 'auth@test.com',
|
email: 'auth@test.com',
|
||||||
@@ -116,9 +118,25 @@ describe('Album service', () => {
|
|||||||
removeAssets: jest.fn(),
|
removeAssets: jest.fn(),
|
||||||
removeUser: jest.fn(),
|
removeUser: jest.fn(),
|
||||||
updateAlbum: jest.fn(),
|
updateAlbum: jest.fn(),
|
||||||
getListByAssetId: jest.fn()
|
getListByAssetId: jest.fn(),
|
||||||
|
getCountByUserId: jest.fn(),
|
||||||
};
|
};
|
||||||
sut = new AlbumService(albumRepositoryMock);
|
|
||||||
|
assetRepositoryMock = {
|
||||||
|
create: jest.fn(),
|
||||||
|
getAllByUserId: jest.fn(),
|
||||||
|
getAllByDeviceId: jest.fn(),
|
||||||
|
getAssetCountByTimeBucket: jest.fn(),
|
||||||
|
getById: jest.fn(),
|
||||||
|
getDetectedObjectsByUserId: jest.fn(),
|
||||||
|
getLocationsByUserId: jest.fn(),
|
||||||
|
getSearchPropertiesByUserId: jest.fn(),
|
||||||
|
getAssetByTimeBucket: jest.fn(),
|
||||||
|
getAssetByChecksum: jest.fn(),
|
||||||
|
getAssetCountByUserId: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sut = new AlbumService(albumRepositoryMock, assetRepositoryMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates album', async () => {
|
it('creates album', async () => {
|
||||||
|
|||||||
@@ -9,10 +9,15 @@ import { UpdateAlbumDto } from './dto/update-album.dto';
|
|||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||||
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
|
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
|
||||||
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
|
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
|
||||||
|
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||||
|
import { ASSET_REPOSITORY, IAssetRepository } from '../asset/asset-repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AlbumService {
|
export class AlbumService {
|
||||||
constructor(@Inject(ALBUM_REPOSITORY) private _albumRepository: IAlbumRepository) {}
|
constructor(
|
||||||
|
@Inject(ALBUM_REPOSITORY) private _albumRepository: IAlbumRepository,
|
||||||
|
@Inject(ASSET_REPOSITORY) private _assetRepository: IAssetRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
private async _getAlbum({
|
private async _getAlbum({
|
||||||
authUser,
|
authUser,
|
||||||
@@ -53,6 +58,11 @@ export class AlbumService {
|
|||||||
return albums.map(mapAlbumExcludeAssetInfo);
|
return albums.map(mapAlbumExcludeAssetInfo);
|
||||||
}
|
}
|
||||||
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
|
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
|
||||||
|
|
||||||
|
for (const album of albums) {
|
||||||
|
await this._checkValidThumbnail(album);
|
||||||
|
}
|
||||||
|
|
||||||
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
|
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,4 +128,22 @@ export class AlbumService {
|
|||||||
const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto);
|
const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto);
|
||||||
return mapAlbum(updatedAlbum);
|
return mapAlbum(updatedAlbum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAlbumCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
|
||||||
|
return this._albumRepository.getCountByUserId(authUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _checkValidThumbnail(album: AlbumEntity): Promise<AlbumEntity> {
|
||||||
|
const assetId = album.albumThumbnailAssetId;
|
||||||
|
if (assetId) {
|
||||||
|
try {
|
||||||
|
await this._assetRepository.getById(assetId);
|
||||||
|
} catch (e) {
|
||||||
|
album.albumThumbnailAssetId = null;
|
||||||
|
return await this._albumRepository.updateAlbum(album, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return album;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsNotEmpty, IsOptional } from 'class-validator';
|
import { IsOptional } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateAlbumDto {
|
export class UpdateAlbumDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class AlbumCountResponseDto {
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
owned!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
shared!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
sharing!: number;
|
||||||
|
|
||||||
|
constructor(owned: number, shared: number, sharing: number) {
|
||||||
|
this.owned = owned;
|
||||||
|
this.shared = shared;
|
||||||
|
this.sharing = sharing;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon
|
|||||||
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
||||||
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||||
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
create(
|
create(
|
||||||
@@ -25,6 +26,7 @@ export interface IAssetRepository {
|
|||||||
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
|
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
|
||||||
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
|
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
|
||||||
getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum): Promise<AssetCountByTimeBucket[]>;
|
getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum): Promise<AssetCountByTimeBucket[]>;
|
||||||
|
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
|
||||||
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
|
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
|
||||||
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
|
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
|
||||||
}
|
}
|
||||||
@@ -38,6 +40,28 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto> {
|
||||||
|
// Get asset count by AssetType
|
||||||
|
const res = await this.assetRepository
|
||||||
|
.createQueryBuilder('asset')
|
||||||
|
.select(`COUNT(asset.id)`, 'count')
|
||||||
|
.addSelect(`asset.type`, 'type')
|
||||||
|
.where('"userId" = :userId', { userId: userId })
|
||||||
|
.groupBy('asset.type')
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
const assetCountByUserId = new AssetCountByUserIdResponseDto(0, 0);
|
||||||
|
res.map((item) => {
|
||||||
|
if (item.type === 'IMAGE') {
|
||||||
|
assetCountByUserId.photos = item.count;
|
||||||
|
} else if (item.type === 'VIDEO') {
|
||||||
|
assetCountByUserId.videos = item.count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return assetCountByUserId;
|
||||||
|
}
|
||||||
|
|
||||||
async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
|
async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
|
||||||
// Get asset entity from a list of time buckets
|
// Get asset entity from a list of time buckets
|
||||||
return await this.assetRepository
|
return await this.assetRepository
|
||||||
@@ -147,6 +171,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
.createQueryBuilder('asset')
|
.createQueryBuilder('asset')
|
||||||
.where('asset.userId = :userId', { userId: userId })
|
.where('asset.userId = :userId', { userId: userId })
|
||||||
.andWhere('asset.resizePath is not NULL')
|
.andWhere('asset.resizePath is not NULL')
|
||||||
|
.andWhere('asset.type = :type', { type: AssetType.IMAGE })
|
||||||
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
|
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
|
||||||
.orderBy('asset.createdAt', 'DESC');
|
.orderBy('asset.createdAt', 'DESC');
|
||||||
|
|
||||||
@@ -201,6 +226,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
where: {
|
where: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
|
type: AssetType.IMAGE,
|
||||||
},
|
},
|
||||||
select: ['deviceAssetId'],
|
select: ['deviceAssetId'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
Header,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
@@ -34,7 +33,7 @@ import { IAssetUploadedJob } from '@app/job/index';
|
|||||||
import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
|
import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
|
||||||
import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant';
|
import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant';
|
||||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||||
import { AssetResponseDto } from './response-dto/asset-response.dto';
|
import { AssetResponseDto } from './response-dto/asset-response.dto';
|
||||||
@@ -48,6 +47,7 @@ import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by
|
|||||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||||
import { QueryFailedError } from 'typeorm';
|
import { QueryFailedError } from 'typeorm';
|
||||||
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -78,7 +78,13 @@ export class AssetController {
|
|||||||
const checksum = await this.assetService.calculateChecksum(file.path);
|
const checksum = await this.assetService.calculateChecksum(file.path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype, checksum);
|
const savedAsset = await this.assetService.createUserAsset(
|
||||||
|
authUser,
|
||||||
|
assetInfo,
|
||||||
|
file.path,
|
||||||
|
file.mimetype,
|
||||||
|
checksum,
|
||||||
|
);
|
||||||
|
|
||||||
if (!savedAsset) {
|
if (!savedAsset) {
|
||||||
await this.backgroundTaskService.deleteFileOnDisk([
|
await this.backgroundTaskService.deleteFileOnDisk([
|
||||||
@@ -104,7 +110,7 @@ export class AssetController {
|
|||||||
]); // simulate asset to make use of delete queue (or use fs.unlink instead)
|
]); // simulate asset to make use of delete queue (or use fs.unlink instead)
|
||||||
|
|
||||||
if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') {
|
if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') {
|
||||||
const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum)
|
const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum);
|
||||||
return new AssetFileUploadResponseDto(existedAsset.id);
|
return new AssetFileUploadResponseDto(existedAsset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +178,10 @@ export class AssetController {
|
|||||||
return this.assetService.getAssetCountByTimeBucket(authUser, getAssetCountByTimeGroupDto);
|
return this.assetService.getAssetCountByTimeBucket(authUser, getAssetCountByTimeGroupDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/count-by-user-id')
|
||||||
|
async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||||
|
return this.assetService.getAssetCountByUserId(authUser);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { AssetRepository, IAssetRepository } from './asset-repository';
|
import { IAssetRepository } from './asset-repository';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
|
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
||||||
|
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
|
|
||||||
describe('AssetService', () => {
|
describe('AssetService', () => {
|
||||||
let sui: AssetService;
|
let sui: AssetService;
|
||||||
@@ -11,7 +14,7 @@ describe('AssetService', () => {
|
|||||||
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
||||||
|
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
const authUser: AuthUserDto = Object.freeze({
|
||||||
id: '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd',
|
id: 'user_id_1',
|
||||||
email: 'auth@test.com',
|
email: 'auth@test.com',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,26 +30,68 @@ describe('AssetService', () => {
|
|||||||
|
|
||||||
return createAssetDto;
|
return createAssetDto;
|
||||||
};
|
};
|
||||||
const _getAsset = () => {
|
|
||||||
const assetEntity = new AssetEntity();
|
|
||||||
|
|
||||||
assetEntity.id = 'e8edabfd-7d8a-45d0-9d61-7c7ca60f2c67';
|
const _getAsset_1 = () => {
|
||||||
assetEntity.userId = '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd';
|
const asset_1 = new AssetEntity();
|
||||||
assetEntity.deviceAssetId = '4967046344801';
|
|
||||||
assetEntity.deviceId = '116766fd-2ef2-52dc-a3ef-149988997291';
|
|
||||||
assetEntity.type = AssetType.VIDEO;
|
|
||||||
assetEntity.originalPath =
|
|
||||||
'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg';
|
|
||||||
assetEntity.resizePath = '';
|
|
||||||
assetEntity.createdAt = '2022-06-19T23:41:36.910Z';
|
|
||||||
assetEntity.modifiedAt = '2022-06-19T23:41:36.910Z';
|
|
||||||
assetEntity.isFavorite = false;
|
|
||||||
assetEntity.mimeType = 'image/jpeg';
|
|
||||||
assetEntity.webpPath = '';
|
|
||||||
assetEntity.encodedVideoPath = '';
|
|
||||||
assetEntity.duration = '0:00:00.000000';
|
|
||||||
|
|
||||||
return assetEntity;
|
asset_1.id = 'id_1';
|
||||||
|
asset_1.userId = 'user_id_1';
|
||||||
|
asset_1.deviceAssetId = 'device_asset_id_1';
|
||||||
|
asset_1.deviceId = 'device_id_1';
|
||||||
|
asset_1.type = AssetType.VIDEO;
|
||||||
|
asset_1.originalPath = 'fake_path/asset_1.jpeg';
|
||||||
|
asset_1.resizePath = '';
|
||||||
|
asset_1.createdAt = '2022-06-19T23:41:36.910Z';
|
||||||
|
asset_1.modifiedAt = '2022-06-19T23:41:36.910Z';
|
||||||
|
asset_1.isFavorite = false;
|
||||||
|
asset_1.mimeType = 'image/jpeg';
|
||||||
|
asset_1.webpPath = '';
|
||||||
|
asset_1.encodedVideoPath = '';
|
||||||
|
asset_1.duration = '0:00:00.000000';
|
||||||
|
return asset_1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getAsset_2 = () => {
|
||||||
|
const asset_2 = new AssetEntity();
|
||||||
|
|
||||||
|
asset_2.id = 'id_2';
|
||||||
|
asset_2.userId = 'user_id_1';
|
||||||
|
asset_2.deviceAssetId = 'device_asset_id_2';
|
||||||
|
asset_2.deviceId = 'device_id_1';
|
||||||
|
asset_2.type = AssetType.VIDEO;
|
||||||
|
asset_2.originalPath = 'fake_path/asset_2.jpeg';
|
||||||
|
asset_2.resizePath = '';
|
||||||
|
asset_2.createdAt = '2022-06-19T23:41:36.910Z';
|
||||||
|
asset_2.modifiedAt = '2022-06-19T23:41:36.910Z';
|
||||||
|
asset_2.isFavorite = false;
|
||||||
|
asset_2.mimeType = 'image/jpeg';
|
||||||
|
asset_2.webpPath = '';
|
||||||
|
asset_2.encodedVideoPath = '';
|
||||||
|
asset_2.duration = '0:00:00.000000';
|
||||||
|
|
||||||
|
return asset_2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getAssets = () => {
|
||||||
|
return [_getAsset_1(), _getAsset_2()];
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getAssetCountByTimeBucket = (): AssetCountByTimeBucket[] => {
|
||||||
|
const result1 = new AssetCountByTimeBucket();
|
||||||
|
result1.count = 2;
|
||||||
|
result1.timeBucket = '2022-06-01T00:00:00.000Z';
|
||||||
|
|
||||||
|
const result2 = new AssetCountByTimeBucket();
|
||||||
|
result1.count = 5;
|
||||||
|
result1.timeBucket = '2022-07-01T00:00:00.000Z';
|
||||||
|
|
||||||
|
return [result1, result2];
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getAssetCountByUserId = (): AssetCountByUserIdResponseDto => {
|
||||||
|
const result = new AssetCountByUserIdResponseDto(2, 2);
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -61,35 +106,72 @@ describe('AssetService', () => {
|
|||||||
getSearchPropertiesByUserId: jest.fn(),
|
getSearchPropertiesByUserId: jest.fn(),
|
||||||
getAssetByTimeBucket: jest.fn(),
|
getAssetByTimeBucket: jest.fn(),
|
||||||
getAssetByChecksum: jest.fn(),
|
getAssetByChecksum: jest.fn(),
|
||||||
|
getAssetCountByUserId: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sui = new AssetService(assetRepositoryMock, a);
|
sui = new AssetService(assetRepositoryMock, a);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Currently failing due to calculate checksum from a file
|
// Currently failing due to calculate checksum from a file
|
||||||
// it('create an asset', async () => {
|
it('create an asset', async () => {
|
||||||
// const assetEntity = _getAsset();
|
const assetEntity = _getAsset_1();
|
||||||
|
|
||||||
// assetRepositoryMock.create.mockImplementation(() => Promise.resolve<AssetEntity>(assetEntity));
|
assetRepositoryMock.create.mockImplementation(() => Promise.resolve<AssetEntity>(assetEntity));
|
||||||
|
|
||||||
// const originalPath =
|
const originalPath = 'fake_path/asset_1.jpeg';
|
||||||
// 'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg';
|
const mimeType = 'image/jpeg';
|
||||||
// const mimeType = 'image/jpeg';
|
const createAssetDto = _getCreateAssetDto();
|
||||||
// const createAssetDto = _getCreateAssetDto();
|
const result = await sui.createUserAsset(
|
||||||
// const result = await sui.createUserAsset(authUser, createAssetDto, originalPath, mimeType);
|
authUser,
|
||||||
|
createAssetDto,
|
||||||
|
originalPath,
|
||||||
|
mimeType,
|
||||||
|
Buffer.from('0x5041E6328F7DF8AFF650BEDAED9251897D9A6241', 'hex'),
|
||||||
|
);
|
||||||
|
|
||||||
// expect(result.userId).toEqual(authUser.id);
|
expect(result.userId).toEqual(authUser.id);
|
||||||
// expect(result.resizePath).toEqual('');
|
expect(result.resizePath).toEqual('');
|
||||||
// expect(result.webpPath).toEqual('');
|
expect(result.webpPath).toEqual('');
|
||||||
// });
|
});
|
||||||
|
|
||||||
it('get assets by device id', async () => {
|
it('get assets by device id', async () => {
|
||||||
assetRepositoryMock.getAllByDeviceId.mockImplementation(() => Promise.resolve<string[]>(['4967046344801']));
|
const assets = _getAssets();
|
||||||
|
|
||||||
const deviceId = '116766fd-2ef2-52dc-a3ef-149988997291';
|
assetRepositoryMock.getAllByDeviceId.mockImplementation(() =>
|
||||||
|
Promise.resolve<string[]>(Array.from(assets.map((asset) => asset.deviceAssetId))),
|
||||||
|
);
|
||||||
|
|
||||||
|
const deviceId = 'device_id_1';
|
||||||
const result = await sui.getUserAssetsByDeviceId(authUser, deviceId);
|
const result = await sui.getUserAssetsByDeviceId(authUser, deviceId);
|
||||||
|
|
||||||
expect(result.length).toEqual(1);
|
expect(result.length).toEqual(2);
|
||||||
expect(result[0]).toEqual('4967046344801');
|
expect(result).toEqual(assets.map((asset) => asset.deviceAssetId));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('get assets count by time bucket', async () => {
|
||||||
|
const assetCountByTimeBucket = _getAssetCountByTimeBucket();
|
||||||
|
|
||||||
|
assetRepositoryMock.getAssetCountByTimeBucket.mockImplementation(() =>
|
||||||
|
Promise.resolve<AssetCountByTimeBucket[]>(assetCountByTimeBucket),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await sui.getAssetCountByTimeBucket(authUser, {
|
||||||
|
timeGroup: TimeGroupEnum.Month,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.totalCount).toEqual(assetCountByTimeBucket.reduce((a, b) => a + b.count, 0));
|
||||||
|
expect(result.buckets.length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('get asset count by user id', async () => {
|
||||||
|
const assetCount = _getAssetCountByUserId();
|
||||||
|
|
||||||
|
assetRepositoryMock.getAssetCountByUserId.mockImplementation(() =>
|
||||||
|
Promise.resolve<AssetCountByUserIdResponseDto>(assetCount),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await sui.getAssetCountByUserId(authUser);
|
||||||
|
|
||||||
|
expect(result).toEqual(assetCount);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import {
|
|||||||
} from './response-dto/asset-count-by-time-group-response.dto';
|
} from './response-dto/asset-count-by-time-group-response.dto';
|
||||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||||
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
|
import { timeUtils } from '@app/common/utils';
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
@@ -55,6 +57,18 @@ export class AssetService {
|
|||||||
mimeType: string,
|
mimeType: string,
|
||||||
checksum: Buffer,
|
checksum: Buffer,
|
||||||
): Promise<AssetEntity> {
|
): Promise<AssetEntity> {
|
||||||
|
// Check valid time.
|
||||||
|
const createdAt = createAssetDto.createdAt;
|
||||||
|
const modifiedAt = createAssetDto.modifiedAt;
|
||||||
|
|
||||||
|
if (!timeUtils.checkValidTimestamp(createdAt)) {
|
||||||
|
createAssetDto.createdAt = await timeUtils.getTimestampFromExif(originalPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeUtils.checkValidTimestamp(modifiedAt)) {
|
||||||
|
createAssetDto.modifiedAt = await timeUtils.getTimestampFromExif(originalPath);
|
||||||
|
}
|
||||||
|
|
||||||
const assetEntity = await this._assetRepository.create(
|
const assetEntity = await this._assetRepository.create(
|
||||||
createAssetDto,
|
createAssetDto,
|
||||||
authUser.id,
|
authUser.id,
|
||||||
@@ -479,4 +493,8 @@ export class AssetService {
|
|||||||
fileReadStream.pipe(sha1Hash);
|
fileReadStream.pipe(sha1Hash);
|
||||||
return deferred;
|
return deferred;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||||
|
return this._assetRepository.getAssetCountByUserId(authUser.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { IsOptional } from 'class-validator';
|
||||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
|
||||||
|
|
||||||
export enum GetAssetThumbnailFormatEnum {
|
export enum GetAssetThumbnailFormatEnum {
|
||||||
JPEG = 'JPEG',
|
JPEG = 'JPEG',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform, Type } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator';
|
import { IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
export class ServeFileDto {
|
export class ServeFileDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class AssetCountByUserIdResponseDto {
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
photos!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
videos!: number;
|
||||||
|
|
||||||
|
constructor(photos: number, videos: number) {
|
||||||
|
this.photos = photos;
|
||||||
|
this.videos = videos;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
import { Body, Controller, Post, Res, UseGuards, ValidationPipe } from '@nestjs/common';
|
import { Body, Controller, Post, Res, UseGuards, ValidationPipe } from '@nestjs/common';
|
||||||
import {
|
import { ApiBadRequestResponse, ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
ApiBadRequestResponse,
|
|
||||||
ApiBearerAuth,
|
|
||||||
ApiBody,
|
|
||||||
ApiCreatedResponse,
|
|
||||||
ApiResponse,
|
|
||||||
ApiTags,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { UserEntity } from '@app/database/entities/user.entity';
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
|
import { ApiResponseProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class LoginResponseDto {
|
export class LoginResponseDto {
|
||||||
@ApiResponseProperty()
|
@ApiResponseProperty()
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { DeviceType } from '@app/database/entities/device-info.entity';
|
import { DeviceType } from '@app/database/entities/device-info.entity';
|
||||||
import { PartialType } from '@nestjs/mapped-types';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNotEmpty, IsOptional } from 'class-validator';
|
import { IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
import { CreateDeviceInfoDto } from './create-device-info.dto';
|
|
||||||
|
|
||||||
export class UpdateDeviceInfoDto {
|
export class UpdateDeviceInfoDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
|
||||||
import { ServerInfoService } from './server-info.service';
|
import { ServerInfoService } from './server-info.service';
|
||||||
import { serverVersion } from '../../constants/server_version.constant';
|
import { serverVersion } from '../../constants/server_version.constant';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
|
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
|
||||||
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
|
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
|
||||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
|
|
||||||
@ApiTags('Server Info')
|
@ApiTags('Server Info')
|
||||||
@Controller('server-info')
|
@Controller('server-info')
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
UploadedFile,
|
UploadedFile,
|
||||||
Response,
|
Response,
|
||||||
Request,
|
Request,
|
||||||
StreamableFile,
|
|
||||||
ParseBoolPipe,
|
ParseBoolPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
@@ -75,8 +74,11 @@ export class UserController {
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Put()
|
@Put()
|
||||||
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
|
async updateUser(
|
||||||
return await this.userService.updateUser(updateUserDto);
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
|
@Body(ValidationPipe) updateUserDto: UpdateUserDto,
|
||||||
|
): Promise<UserResponseDto> {
|
||||||
|
return await this.userService.updateUser(authUser, updateUserDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseInterceptors(FileInterceptor('file', profileImageUploadOption))
|
@UseInterceptors(FileInterceptor('file', profileImageUploadOption))
|
||||||
|
|||||||
137
server/apps/immich/src/api-v1/user/user.service.spec.ts
Normal file
137
server/apps/immich/src/api-v1/user/user.service.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
|
import { IUserRepository } from './user-repository';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
|
describe('UserService', () => {
|
||||||
|
let sui: UserService;
|
||||||
|
let userRepositoryMock: jest.Mocked<IUserRepository>;
|
||||||
|
|
||||||
|
const adminAuthUser: AuthUserDto = Object.freeze({
|
||||||
|
id: 'admin_id',
|
||||||
|
email: 'admin@test.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
const immichAuthUser: AuthUserDto = Object.freeze({
|
||||||
|
id: 'immich_id',
|
||||||
|
email: 'immich@test.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminUser: UserEntity = Object.freeze({
|
||||||
|
id: 'admin_id',
|
||||||
|
email: 'admin@test.com',
|
||||||
|
password: 'admin_password',
|
||||||
|
salt: 'admin_salt',
|
||||||
|
firstName: 'admin_first_name',
|
||||||
|
lastName: 'admin_last_name',
|
||||||
|
isAdmin: true,
|
||||||
|
shouldChangePassword: false,
|
||||||
|
profileImagePath: '',
|
||||||
|
createdAt: '2021-01-01',
|
||||||
|
});
|
||||||
|
|
||||||
|
const immichUser: UserEntity = Object.freeze({
|
||||||
|
id: 'immich_id',
|
||||||
|
email: 'immich@test.com',
|
||||||
|
password: 'immich_password',
|
||||||
|
salt: 'immich_salt',
|
||||||
|
firstName: 'immich_first_name',
|
||||||
|
lastName: 'immich_last_name',
|
||||||
|
isAdmin: false,
|
||||||
|
shouldChangePassword: false,
|
||||||
|
profileImagePath: '',
|
||||||
|
createdAt: '2021-01-01',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedImmichUser: UserEntity = Object.freeze({
|
||||||
|
id: 'immich_id',
|
||||||
|
email: 'immich@test.com',
|
||||||
|
password: 'immich_password',
|
||||||
|
salt: 'immich_salt',
|
||||||
|
firstName: 'updated_immich_first_name',
|
||||||
|
lastName: 'updated_immich_last_name',
|
||||||
|
isAdmin: false,
|
||||||
|
shouldChangePassword: true,
|
||||||
|
profileImagePath: '',
|
||||||
|
createdAt: '2021-01-01',
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
userRepositoryMock = {
|
||||||
|
create: jest.fn(),
|
||||||
|
createProfileImage: jest.fn(),
|
||||||
|
get: jest.fn(),
|
||||||
|
getByEmail: jest.fn(),
|
||||||
|
getList: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sui = new UserService(userRepositoryMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(sui).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Update user', () => {
|
||||||
|
it('should update user', () => {
|
||||||
|
const requestor = immichAuthUser;
|
||||||
|
const userToUpdate = immichUser;
|
||||||
|
|
||||||
|
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(immichUser));
|
||||||
|
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(userToUpdate));
|
||||||
|
userRepositoryMock.update.mockImplementationOnce(() => Promise.resolve(updatedImmichUser));
|
||||||
|
|
||||||
|
const result = sui.updateUser(requestor, {
|
||||||
|
id: userToUpdate.id,
|
||||||
|
shouldChangePassword: true,
|
||||||
|
});
|
||||||
|
expect(result).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('user can only update its information', () => {
|
||||||
|
const requestor = immichAuthUser;
|
||||||
|
|
||||||
|
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(immichUser));
|
||||||
|
|
||||||
|
const result = sui.updateUser(requestor, {
|
||||||
|
id: 'not_immich_auth_user_id',
|
||||||
|
password: 'I take over your account now',
|
||||||
|
});
|
||||||
|
expect(result).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('admin can update any user information', async () => {
|
||||||
|
const requestor = adminAuthUser;
|
||||||
|
const userToUpdate = immichUser;
|
||||||
|
|
||||||
|
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(adminUser));
|
||||||
|
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(userToUpdate));
|
||||||
|
userRepositoryMock.update.mockImplementationOnce(() => Promise.resolve(updatedImmichUser));
|
||||||
|
|
||||||
|
const result = await sui.updateUser(requestor, {
|
||||||
|
id: userToUpdate.id,
|
||||||
|
shouldChangePassword: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.id).toEqual(updatedImmichUser.id);
|
||||||
|
expect(result.shouldChangePassword).toEqual(updatedImmichUser.shouldChangePassword);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update user information should throw error if user not found', () => {
|
||||||
|
const requestor = adminAuthUser;
|
||||||
|
const userToUpdate = immichUser;
|
||||||
|
|
||||||
|
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(adminUser));
|
||||||
|
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(null));
|
||||||
|
|
||||||
|
const result = sui.updateUser(requestor, {
|
||||||
|
id: userToUpdate.id,
|
||||||
|
shouldChangePassword: true,
|
||||||
|
});
|
||||||
|
expect(result).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -78,7 +78,19 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
|
async updateUser(authUser: AuthUserDto, updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
|
||||||
|
const requestor = await this.userRepository.get(authUser.id);
|
||||||
|
|
||||||
|
if (!requestor) {
|
||||||
|
throw new NotFoundException('Requestor not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestor.isAdmin) {
|
||||||
|
if (requestor.id !== updateUserDto.id) {
|
||||||
|
throw new BadRequestException('Unauthorized');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.userRepository.get(updateUserDto.id);
|
const user = await this.userRepository.get(updateUserDto.id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
@@ -88,8 +100,8 @@ export class UserService {
|
|||||||
|
|
||||||
return mapUser(updatedUser);
|
return mapUser(updatedUser);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(e, 'Create new user');
|
Logger.error(e, 'Failed to update user info');
|
||||||
throw new InternalServerErrorException('Failed to register new user');
|
throw new InternalServerErrorException('Failed to update user info');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export class AppModule implements NestModule {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
configure(consumer: MiddlewareConsumer): void {
|
configure(consumer: MiddlewareConsumer): void {
|
||||||
if (process.env.NODE_ENV == 'development') {
|
if (process.env.NODE_ENV == 'development') {
|
||||||
// consumer.apply(AppLoggerMiddleware).forRoutes('*');
|
consumer.apply(AppLoggerMiddleware).forRoutes('*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { diskStorage } from 'multer';
|
|||||||
import { extname, join } from 'path';
|
import { extname, join } from 'path';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
import sanitize from 'sanitize-filename';
|
||||||
|
|
||||||
export const assetUploadOption: MulterOptions = {
|
export const assetUploadOption: MulterOptions = {
|
||||||
fileFilter: (req: Request, file: any, cb: any) => {
|
fileFilter: (req: Request, file: any, cb: any) => {
|
||||||
@@ -19,17 +20,13 @@ export const assetUploadOption: MulterOptions = {
|
|||||||
storage: diskStorage({
|
storage: diskStorage({
|
||||||
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||||
const basePath = APP_UPLOAD_LOCATION;
|
const basePath = APP_UPLOAD_LOCATION;
|
||||||
// TODO these are currently not used. Shall we remove them?
|
|
||||||
// const fileInfo = req.body as CreateAssetDto;
|
|
||||||
|
|
||||||
// const yearInfo = new Date(fileInfo.createdAt).getFullYear();
|
|
||||||
// const monthInfo = new Date(fileInfo.createdAt).getMonth();
|
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalUploadFolder = join(basePath, req.user.id, 'original', req.body['deviceId']);
|
const sanitizedDeviceId = sanitize(String(req.body['deviceId']));
|
||||||
|
const originalUploadFolder = join(basePath, req.user.id, 'original', sanitizedDeviceId);
|
||||||
|
|
||||||
if (!existsSync(originalUploadFolder)) {
|
if (!existsSync(originalUploadFolder)) {
|
||||||
mkdirSync(originalUploadFolder, { recursive: true });
|
mkdirSync(originalUploadFolder, { recursive: true });
|
||||||
@@ -41,8 +38,9 @@ export const assetUploadOption: MulterOptions = {
|
|||||||
|
|
||||||
filename: (req: Request, file: Express.Multer.File, cb: any) => {
|
filename: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||||
const fileNameUUID = randomUUID();
|
const fileNameUUID = randomUUID();
|
||||||
|
const fileName = `${fileNameUUID}${req.body['fileExtension'].toLowerCase()}`;
|
||||||
|
|
||||||
cb(null, `${fileNameUUID}${req.body['fileExtension'].toLowerCase()}`);
|
cb(null, sanitize(fileName));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user