Compare commits
34 Commits
v1.32.0_50
...
v1.33.0_52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25848b78f9 | ||
|
|
f94176a910 | ||
|
|
ae96508e15 | ||
|
|
95ebf815eb | ||
|
|
b713fb5650 | ||
|
|
6159c83fd2 | ||
|
|
f1af17bf4d | ||
|
|
a87c1c1210 | ||
|
|
e63d165b65 | ||
|
|
9411770253 | ||
|
|
dc80ac1c88 | ||
|
|
bb055628cc | ||
|
|
390bcdb8c6 | ||
|
|
d95bcb46ad | ||
|
|
7b954e21e7 | ||
|
|
a6eea4d096 | ||
|
|
2c189d5c78 | ||
|
|
85a80fd032 | ||
|
|
0309b47515 | ||
|
|
95d8f60389 | ||
|
|
1ec7122381 | ||
|
|
061b229e12 | ||
|
|
3617433858 | ||
|
|
d6d525cc1b | ||
|
|
e752290458 | ||
|
|
d77e25425e | ||
|
|
028c0249a3 | ||
|
|
a3ca5307a5 | ||
|
|
6796462b13 | ||
|
|
d08475d5af | ||
|
|
d310c77fc8 | ||
|
|
75d8ca1306 | ||
|
|
894eea739e | ||
|
|
1156290377 |
@@ -20,7 +20,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.repository == 'immich-app/immich' }}
|
||||
uses: docker/login-action@v2
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.repository == 'immich-app/immich' }}
|
||||
uses: docker/login-action@v2
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.repository == 'immich-app/immich' }}
|
||||
uses: docker/login-action@v2
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.repository == 'immich-app/immich' }}
|
||||
uses: docker/login-action@v2
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.1.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
|
||||
21
.github/workflows/dispatch_sdk_update.yml
vendored
Normal file
21
.github/workflows/dispatch_sdk_update.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Update Immich SDK
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
update-sdk-repos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'immich-app',
|
||||
repo: 'immich-sdk-typescript-axios',
|
||||
workflow_id: 'build.yml',
|
||||
ref: 'main'
|
||||
})
|
||||
83
.github/workflows/openapi-generator.yml
vendored
83
.github/workflows/openapi-generator.yml
vendored
@@ -1,83 +0,0 @@
|
||||
name: Generate OpenAPI SDK
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
generate-typescript-axios:
|
||||
runs-on: ubuntu-latest
|
||||
name: OpenAPI Generator
|
||||
steps:
|
||||
# Checkout your code
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
# Use the action to generate a client package
|
||||
# This uses the default path for the openapi document and thus assumes there is an openapi.json in the current workspace.
|
||||
- name: Generate Typescript Axios Client
|
||||
uses: openapi-generators/openapitools-generator-action@v1
|
||||
with:
|
||||
generator: typescript-axios
|
||||
generator-tag: v6.2.0
|
||||
openapi-file: server/immich-openapi-specs.json
|
||||
|
||||
# Do something with the generated client (likely publishing it somewhere)
|
||||
- name: Push to typescript repo
|
||||
run: |
|
||||
git config --global init.defaultBranch main
|
||||
git config --global pull.rebase false
|
||||
git config --global user.email "alex.tran1502@gmail.com"
|
||||
git config --global user.name "Alex Tran"
|
||||
cd typescript-axios-client
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Update SDK"
|
||||
git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-typescript-axios.git
|
||||
git pull origin main --allow-unrelated-histories
|
||||
git push origin main 2>&1 | grep -v 'To https'
|
||||
|
||||
- name: Generate Dart SDK
|
||||
uses: openapi-generators/openapitools-generator-action@v1
|
||||
with:
|
||||
generator: dart
|
||||
generator-tag: v6.2.0
|
||||
openapi-file: server/immich-openapi-specs.json
|
||||
|
||||
- name: Push to Dart repo
|
||||
run: |
|
||||
git config --global init.defaultBranch main
|
||||
git config --global pull.rebase false
|
||||
git config --global user.email "alex.tran1502@gmail.com"
|
||||
git config --global user.name "Alex Tran"
|
||||
cd dart-client
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Update SDK"
|
||||
git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-dart.git
|
||||
git pull origin main --allow-unrelated-histories
|
||||
git push origin main 2>&1 | grep -v 'To https'
|
||||
|
||||
- name: Generate Rust SDK
|
||||
uses: openapi-generators/openapitools-generator-action@v1
|
||||
with:
|
||||
generator: rust
|
||||
generator-tag: v6.2.0
|
||||
openapi-file: server/immich-openapi-specs.json
|
||||
|
||||
- name: Push to Rust repo
|
||||
run: |
|
||||
git config --global init.defaultBranch main
|
||||
git config --global pull.rebase false
|
||||
git config --global user.email "alex.tran1502@gmail.com"
|
||||
git config --global user.name "Alex Tran"
|
||||
cd rust-client
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Update SDK"
|
||||
git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-rust.git
|
||||
git pull origin main --allow-unrelated-histories
|
||||
git push origin main 2>&1 | grep -v 'To https'
|
||||
@@ -116,8 +116,7 @@ There are several services that compose Immich:
|
||||
|
||||
# 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).
|
||||
|
||||
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)
|
||||
|
||||
> ⚠️ *This installation method is for evaluating Immich before further customization to meet the users' needs.*
|
||||
@@ -197,6 +196,10 @@ If you have installed, you can update the application by navigate to the directo
|
||||
```bash
|
||||
docker-compose pull && docker-compose up -d
|
||||
```
|
||||
# Unraid Installation
|
||||
|
||||
Please follow this [article](https://mfaz.dev/posts/immich-unraid/) for a tutorial on how to install Immich on Unraid
|
||||
|
||||
|
||||
# Mobile app
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
||||
RUN apt-get update > /dev/null \
|
||||
&& apt-get install --no-install-recommends -y gcc g++ make cmake python3 python3-pip ffmpeg > /dev/null \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN npm ci
|
||||
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
|
||||
@@ -23,6 +24,9 @@ FROM node:16-bullseye-slim
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
@@ -30,13 +34,18 @@ COPY entrypoint.sh ./
|
||||
|
||||
RUN mkdir -p /usr/src/app/dist \
|
||||
&& mkdir -p /usr/src/app/node_modules \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y ffmpeg \
|
||||
&& rm -rf /var/cache/apt/lists
|
||||
&& mkdir -p /usr/src/app/.reverse-geocoding-dump \
|
||||
&& apt-get update > /dev/null \
|
||||
&& apt-get install --no-install-recommends -y ffmpeg > /dev/null \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/cache/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=builder /usr/src/app/dist ./dist
|
||||
|
||||
RUN npm prune --production
|
||||
|
||||
# CMD [ "node", "dist/main" ]
|
||||
RUN chown -R node:0 /usr/src/app \
|
||||
&& chmod -R g=u /usr/src/app
|
||||
|
||||
RUN addgroup node root
|
||||
21
machine-learning/LICENSE
Normal file
21
machine-learning/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Hau Tran
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 50,
|
||||
"android.injected.version.name" => "1.32.0",
|
||||
"android.injected.version.code" => 52,
|
||||
"android.injected.version.name" => "1.33.0",
|
||||
}
|
||||
)
|
||||
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 @@
|
||||
* Improved slow initial loading for large amount of asset.
|
||||
@@ -0,0 +1,3 @@
|
||||
* Fixed back button navigation
|
||||
* Added duplicated asset upload handling mechanism
|
||||
* Fixed cannot signout completely when "Save Logged In" is checked
|
||||
@@ -24,6 +24,7 @@
|
||||
"backup_controller_page_backup_selected": "Ausgewählt: ",
|
||||
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
|
||||
"backup_controller_page_cancel": "Abbrechen",
|
||||
"backup_background_service_default_notification": "Suche nach neuen assets…",
|
||||
"backup_controller_page_created": "Erstellt: {}",
|
||||
"backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.",
|
||||
"backup_controller_page_excluded": "Ausgeschlossen: ",
|
||||
@@ -123,4 +124,4 @@
|
||||
"version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ",
|
||||
"version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
|
||||
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.32.0"
|
||||
version_number: "1.33.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -25,3 +25,7 @@ const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
|
||||
const String backupFailedSince = "immichBackupFailedSince"; // Key 1
|
||||
const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2
|
||||
const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3
|
||||
|
||||
// Duplicate asset
|
||||
const String duplicatedAssetsBox = "immichDuplicatedAssetsBox"; // Box
|
||||
const String duplicatedAssetsKey = "immichDuplicatedAssetsKey"; // Key 1
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
@@ -30,12 +31,14 @@ void main() async {
|
||||
|
||||
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
||||
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
||||
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
|
||||
|
||||
await Hive.openBox(userInfoBox);
|
||||
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
||||
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||
await Hive.openBox(hiveGithubReleaseInfoBox);
|
||||
await Hive.openBox(userSettingInfoBox);
|
||||
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||
AlbumNotifier(this._albumService) : super([]);
|
||||
AlbumNotifier(this._albumService, this._albumCacheService) : super([]);
|
||||
final AlbumService _albumService;
|
||||
final AlbumCacheService _albumCacheService;
|
||||
|
||||
_cacheState() {
|
||||
_albumCacheService.put(state);
|
||||
}
|
||||
|
||||
getAllAlbums() async {
|
||||
|
||||
if (await _albumCacheService.isValid() && state.isEmpty) {
|
||||
state = await _albumCacheService.get();
|
||||
}
|
||||
|
||||
List<AlbumResponseDto>? albums =
|
||||
await _albumService.getAlbums(isShared: false);
|
||||
|
||||
if (albums != null) {
|
||||
state = albums;
|
||||
_cacheState();
|
||||
}
|
||||
}
|
||||
|
||||
deleteAlbum(String albumId) {
|
||||
state = state.where((album) => album.id != albumId).toList();
|
||||
_cacheState();
|
||||
}
|
||||
|
||||
Future<AlbumResponseDto?> createAlbum(
|
||||
@@ -28,6 +41,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||
|
||||
if (album != null) {
|
||||
state = [...state, album];
|
||||
_cacheState();
|
||||
|
||||
return album;
|
||||
}
|
||||
return null;
|
||||
@@ -36,5 +51,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||
|
||||
final albumProvider =
|
||||
StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
|
||||
return AlbumNotifier(ref.watch(albumServiceProvider));
|
||||
return AlbumNotifier(
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(albumCacheServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
|
||||
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]);
|
||||
|
||||
final AlbumService _sharedAlbumService;
|
||||
final SharedAlbumCacheService _sharedAlbumCacheService;
|
||||
|
||||
_cacheState() {
|
||||
_sharedAlbumCacheService.put(state);
|
||||
}
|
||||
|
||||
Future<AlbumResponseDto?> createSharedAlbum(
|
||||
String albumName,
|
||||
@@ -22,6 +28,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||
|
||||
if (newAlbum != null) {
|
||||
state = [...state, newAlbum];
|
||||
_cacheState();
|
||||
}
|
||||
|
||||
return newAlbum;
|
||||
@@ -33,16 +40,22 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||
}
|
||||
|
||||
getAllSharedAlbums() async {
|
||||
if (await _sharedAlbumCacheService.isValid() && state.isEmpty) {
|
||||
state = await _sharedAlbumCacheService.get();
|
||||
}
|
||||
|
||||
List<AlbumResponseDto>? sharedAlbums =
|
||||
await _sharedAlbumService.getAlbums(isShared: true);
|
||||
|
||||
if (sharedAlbums != null) {
|
||||
state = sharedAlbums;
|
||||
_cacheState();
|
||||
}
|
||||
}
|
||||
|
||||
deleteAlbum(String albumId) async {
|
||||
state = state.where((album) => album.id != albumId).toList();
|
||||
_cacheState();
|
||||
}
|
||||
|
||||
Future<bool> leaveAlbum(String albumId) async {
|
||||
@@ -50,6 +63,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||
|
||||
if (res) {
|
||||
state = state.where((album) => album.id != albumId).toList();
|
||||
_cacheState();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -72,7 +86,10 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||
|
||||
final sharedAlbumProvider =
|
||||
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
|
||||
return SharedAlbumNotifier(ref.watch(albumServiceProvider));
|
||||
return SharedAlbumNotifier(
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(sharedAlbumCacheServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
||||
|
||||
49
mobile/lib/modules/album/services/album_cache.service.dart
Normal file
49
mobile/lib/modules/album/services/album_cache.service.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/services/json_cache.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class BaseAlbumCacheService extends JsonCache<List<AlbumResponseDto>> {
|
||||
BaseAlbumCacheService(super.cacheFileName);
|
||||
|
||||
@override
|
||||
void put(List<AlbumResponseDto> data) {
|
||||
putRawData(data.map((e) => e.toJson()).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AlbumResponseDto>> get() async {
|
||||
try {
|
||||
final mapList = await readRawData() as List<dynamic>;
|
||||
|
||||
final responseData = mapList
|
||||
.map((e) => AlbumResponseDto.fromJson(e))
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
|
||||
return responseData;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumCacheService extends BaseAlbumCacheService {
|
||||
AlbumCacheService() : super("album_cache");
|
||||
}
|
||||
|
||||
class SharedAlbumCacheService extends BaseAlbumCacheService {
|
||||
SharedAlbumCacheService() : super("shared_album_cache");
|
||||
}
|
||||
|
||||
final albumCacheServiceProvider = Provider(
|
||||
(ref) => AlbumCacheService(),
|
||||
);
|
||||
|
||||
final sharedAlbumCacheServiceProvider = Provider(
|
||||
(ref) => SharedAlbumCacheService(),
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/modules/backup/background_service/localization.dar
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
@@ -316,10 +317,13 @@ class BackgroundService {
|
||||
|
||||
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
||||
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
||||
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
|
||||
|
||||
await Hive.openBox(userInfoBox);
|
||||
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
||||
await Hive.openBox(userSettingInfoBox);
|
||||
await Hive.openBox(backgroundBackupInfoBox);
|
||||
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
|
||||
|
||||
ApiService apiService = ApiService();
|
||||
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
||||
@@ -410,7 +414,7 @@ class BackgroundService {
|
||||
final bool ok = await backupService.backupAsset(
|
||||
toUpload,
|
||||
_cancellationToken!,
|
||||
notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId) {},
|
||||
notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {},
|
||||
notifySingleProgress ? _onProgress : (sent, total) {},
|
||||
notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
|
||||
_onBackupError,
|
||||
@@ -429,7 +433,7 @@ class BackgroundService {
|
||||
return "$percent% ($_uploadedAssetsCount/$_assetsToUploadCount)";
|
||||
}
|
||||
|
||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||
void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) {
|
||||
debugPrint("Uploaded $deviceAssetId from $deviceId");
|
||||
_uploadedAssetsCount++;
|
||||
_updateNotification(
|
||||
|
||||
@@ -45,8 +45,10 @@ class ErrorUploadAsset extends Equatable {
|
||||
List<Object> get props {
|
||||
return [
|
||||
id,
|
||||
createdAt,
|
||||
fileName,
|
||||
fileType,
|
||||
asset,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'hive_duplicated_assets.model.g.dart';
|
||||
|
||||
@HiveType(typeId: 2)
|
||||
class HiveDuplicatedAssets {
|
||||
@HiveField(0, defaultValue: [])
|
||||
List<String> duplicatedAssetIds;
|
||||
|
||||
HiveDuplicatedAssets({
|
||||
required this.duplicatedAssetIds,
|
||||
});
|
||||
|
||||
HiveDuplicatedAssets copyWith({
|
||||
List<String>? duplicatedAssetIds,
|
||||
}) {
|
||||
return HiveDuplicatedAssets(
|
||||
duplicatedAssetIds: duplicatedAssetIds ?? this.duplicatedAssetIds,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'duplicatedAssetIds': duplicatedAssetIds,
|
||||
};
|
||||
}
|
||||
|
||||
factory HiveDuplicatedAssets.fromMap(Map<String, dynamic> map) {
|
||||
return HiveDuplicatedAssets(
|
||||
duplicatedAssetIds: List<String>.from(map['duplicatedAssetIds']),
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory HiveDuplicatedAssets.fromJson(String source) =>
|
||||
HiveDuplicatedAssets.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'HiveDuplicatedAssets(duplicatedAssetIds: $duplicatedAssetIds)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
final listEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
return other is HiveDuplicatedAssets &&
|
||||
listEquals(other.duplicatedAssetIds, duplicatedAssetIds);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => duplicatedAssetIds.hashCode;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'hive_duplicated_assets.model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class HiveDuplicatedAssetsAdapter extends TypeAdapter<HiveDuplicatedAssets> {
|
||||
@override
|
||||
final int typeId = 2;
|
||||
|
||||
@override
|
||||
HiveDuplicatedAssets read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return HiveDuplicatedAssets(
|
||||
duplicatedAssetIds:
|
||||
fields[0] == null ? [] : (fields[0] as List).cast<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, HiveDuplicatedAssets obj) {
|
||||
writer
|
||||
..writeByte(1)
|
||||
..writeByte(0)
|
||||
..write(obj.duplicatedAssetIds);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is HiveDuplicatedAssetsAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
@@ -296,6 +297,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
/// Those assets are unique and are used as the total assets
|
||||
///
|
||||
Future<void> _updateBackupAssetCount() async {
|
||||
Set<String> duplicatedAssetIds = _backupService.getDuplicatedAssetIds();
|
||||
Set<AssetEntity> assetsFromSelectedAlbums = {};
|
||||
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
||||
|
||||
@@ -326,9 +328,15 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
// Find asset that were backup from selected albums
|
||||
Set<String> selectedAlbumsBackupAssets =
|
||||
Set.from(allUniqueAssets.map((e) => e.id));
|
||||
|
||||
selectedAlbumsBackupAssets
|
||||
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
|
||||
|
||||
// Remove duplicated asset from all unique assets
|
||||
allUniqueAssets.removeWhere(
|
||||
(asset) => duplicatedAssetIds.contains(asset.id),
|
||||
);
|
||||
|
||||
if (allUniqueAssets.isEmpty) {
|
||||
debugPrint("No Asset On Device");
|
||||
state = state.copyWith(
|
||||
@@ -455,14 +463,26 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||
state = state.copyWith(
|
||||
selectedAlbumsBackupAssetsIds: {
|
||||
...state.selectedAlbumsBackupAssetsIds,
|
||||
deviceAssetId
|
||||
},
|
||||
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
|
||||
);
|
||||
void _onAssetUploaded(
|
||||
String deviceAssetId,
|
||||
String deviceId,
|
||||
bool isDuplicated,
|
||||
) {
|
||||
if (isDuplicated) {
|
||||
state = state.copyWith(
|
||||
allUniqueAssets: state.allUniqueAssets
|
||||
.where((asset) => asset.id != deviceAssetId)
|
||||
.toSet(),
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
selectedAlbumsBackupAssetsIds: {
|
||||
...state.selectedAlbumsBackupAssetsIds,
|
||||
deviceAssetId
|
||||
},
|
||||
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
|
||||
);
|
||||
}
|
||||
|
||||
if (state.allUniqueAssets.length -
|
||||
state.selectedAlbumsBackupAssetsIds.length ==
|
||||
@@ -564,6 +584,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
albums.lastExcludedBackupTime,
|
||||
);
|
||||
}
|
||||
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
|
||||
final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
|
||||
state = state.copyWith(
|
||||
backupProgress: previous,
|
||||
@@ -608,6 +629,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
} catch (error) {
|
||||
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
||||
}
|
||||
try {
|
||||
if (Hive.isBoxOpen(duplicatedAssetsBox)) {
|
||||
await Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox).close();
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
||||
}
|
||||
try {
|
||||
if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
|
||||
await Hive.box(backgroundBackupInfoBox).close();
|
||||
|
||||
@@ -19,6 +19,8 @@ import 'package:http_parser/http_parser.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:cancellation_token_http/http.dart' as http;
|
||||
|
||||
import '../models/hive_duplicated_assets.model.dart';
|
||||
|
||||
final backupServiceProvider = Provider(
|
||||
(ref) => BackupService(
|
||||
ref.watch(apiServiceProvider),
|
||||
@@ -41,6 +43,29 @@ class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
void _saveDuplicatedAssetIdToLocalStorage(List<String> deviceAssetIds) {
|
||||
HiveDuplicatedAssets duplicatedAssets =
|
||||
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
|
||||
.get(duplicatedAssetsKey) ??
|
||||
HiveDuplicatedAssets(duplicatedAssetIds: []);
|
||||
|
||||
duplicatedAssets.duplicatedAssetIds =
|
||||
{...duplicatedAssets.duplicatedAssetIds, ...deviceAssetIds}.toList();
|
||||
|
||||
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
|
||||
.put(duplicatedAssetsKey, duplicatedAssets);
|
||||
}
|
||||
|
||||
/// Get duplicated asset id from Hive storage
|
||||
Set<String> getDuplicatedAssetIds() {
|
||||
HiveDuplicatedAssets duplicatedAssets =
|
||||
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
|
||||
.get(duplicatedAssetsKey) ??
|
||||
HiveDuplicatedAssets(duplicatedAssetIds: []);
|
||||
|
||||
return duplicatedAssets.duplicatedAssetIds.toSet();
|
||||
}
|
||||
|
||||
/// Returns all assets newer than the last successful backup per album
|
||||
Future<List<AssetEntity>> buildUploadCandidates(
|
||||
HiveBackupAlbums backupAlbums,
|
||||
@@ -140,34 +165,47 @@ class BackupService {
|
||||
Future<List<AssetEntity>> removeAlreadyUploadedAssets(
|
||||
List<AssetEntity> candidates,
|
||||
) async {
|
||||
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||
if (candidates.length < 10) {
|
||||
final List<CheckDuplicateAssetResponseDto?> duplicateResponse =
|
||||
await Future.wait(
|
||||
candidates.map(
|
||||
(e) => _apiService.assetApi.checkDuplicateAsset(
|
||||
CheckDuplicateAssetDto(deviceAssetId: e.id, deviceId: deviceId),
|
||||
),
|
||||
if (candidates.isEmpty) {
|
||||
return candidates;
|
||||
}
|
||||
final Set<String> duplicatedAssetIds = getDuplicatedAssetIds();
|
||||
candidates = duplicatedAssetIds.isEmpty
|
||||
? candidates
|
||||
: candidates
|
||||
.whereNot((asset) => duplicatedAssetIds.contains(asset.id))
|
||||
.toList();
|
||||
if (candidates.isEmpty) {
|
||||
return candidates;
|
||||
}
|
||||
final Set<String> existing = {};
|
||||
try {
|
||||
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||
final CheckExistingAssetsResponseDto? duplicates =
|
||||
await _apiService.assetApi.checkExistingAssets(
|
||||
CheckExistingAssetsDto(
|
||||
deviceAssetIds: candidates.map((e) => e.id).toList(),
|
||||
deviceId: deviceId,
|
||||
),
|
||||
);
|
||||
return candidates
|
||||
.whereIndexed((i, e) => duplicateResponse[i]?.isExist == false)
|
||||
.toList();
|
||||
} else {
|
||||
final List<String>? allAssetsInDatabase = await getDeviceBackupAsset();
|
||||
|
||||
if (allAssetsInDatabase == null) {
|
||||
return candidates;
|
||||
if (duplicates != null) {
|
||||
existing.addAll(duplicates.existingIds);
|
||||
}
|
||||
} on ApiException {
|
||||
// workaround for older server versions or when checking for too many assets at once
|
||||
final List<String>? allAssetsInDatabase = await getDeviceBackupAsset();
|
||||
if (allAssetsInDatabase != null) {
|
||||
existing.addAll(allAssetsInDatabase);
|
||||
}
|
||||
final Set<String> inDb = allAssetsInDatabase.toSet();
|
||||
return candidates.whereNot((e) => inDb.contains(e.id)).toList();
|
||||
}
|
||||
return existing.isEmpty
|
||||
? candidates
|
||||
: candidates.whereNot((e) => existing.contains(e.id)).toList();
|
||||
}
|
||||
|
||||
Future<bool> backupAsset(
|
||||
Iterable<AssetEntity> assetList,
|
||||
http.CancellationToken cancelToken,
|
||||
Function(String, String) singleAssetDoneCb,
|
||||
Function(String, String, bool) uploadSuccessCb,
|
||||
Function(int, int) uploadProgressCb,
|
||||
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
|
||||
Function(ErrorUploadAsset) errorCb,
|
||||
@@ -176,6 +214,7 @@ class BackupService {
|
||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||
File? file;
|
||||
bool anyErrors = false;
|
||||
final List<String> duplicatedAssetIds = [];
|
||||
|
||||
for (var entity in assetList) {
|
||||
try {
|
||||
@@ -235,8 +274,13 @@ class BackupService {
|
||||
|
||||
var response = await req.send(cancellationToken: cancelToken);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
singleAssetDoneCb(entity.id, deviceId);
|
||||
if (response.statusCode == 200) {
|
||||
// asset is a duplicate (already exists on the server)
|
||||
duplicatedAssetIds.add(entity.id);
|
||||
uploadSuccessCb(entity.id, deviceId, true);
|
||||
} else if (response.statusCode == 201) {
|
||||
// stored a new asset on the server
|
||||
uploadSuccessCb(entity.id, deviceId, false);
|
||||
} else {
|
||||
var data = await response.stream.bytesToString();
|
||||
var error = jsonDecode(data);
|
||||
@@ -260,7 +304,8 @@ class BackupService {
|
||||
}
|
||||
} on http.CancelledException {
|
||||
debugPrint("Backup was cancelled by the user");
|
||||
return false;
|
||||
anyErrors = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
debugPrint("ERROR backupAsset: ${e.toString()}");
|
||||
anyErrors = true;
|
||||
@@ -271,6 +316,9 @@ class BackupService {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (duplicatedAssetIds.isNotEmpty) {
|
||||
_saveDuplicatedAssetIdToLocalStorage(duplicatedAssetIds);
|
||||
}
|
||||
return !anyErrors;
|
||||
}
|
||||
|
||||
|
||||
@@ -419,7 +419,6 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
ActionChip(
|
||||
avatar: Icon(
|
||||
Icons.info,
|
||||
size: 24,
|
||||
color: Colors.red[400],
|
||||
),
|
||||
elevation: 1,
|
||||
|
||||
37
mobile/lib/modules/home/services/asset_cache.service.dart
Normal file
37
mobile/lib/modules/home/services/asset_cache.service.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/services/json_cache.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
|
||||
class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
|
||||
AssetCacheService() : super("asset_cache");
|
||||
|
||||
@override
|
||||
void put(List<AssetResponseDto> data) {
|
||||
putRawData(data.map((e) => e.toJson()).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AssetResponseDto>> get() async {
|
||||
try {
|
||||
final mapList = await readRawData() as List<dynamic>;
|
||||
|
||||
final responseData = mapList
|
||||
.map((e) => AssetResponseDto.fromJson(e))
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
|
||||
return responseData;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final assetCacheServiceProvider = Provider(
|
||||
(ref) => AssetCacheService(),
|
||||
);
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter/services.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
@@ -16,6 +18,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
this._deviceInfoService,
|
||||
this._backupService,
|
||||
this._apiService,
|
||||
this._assetCacheService,
|
||||
this._albumCacheService,
|
||||
this._sharedAlbumCacheService,
|
||||
) : super(
|
||||
AuthenticationState(
|
||||
deviceId: "",
|
||||
@@ -42,6 +47,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
final DeviceInfoService _deviceInfoService;
|
||||
final BackupService _backupService;
|
||||
final ApiService _apiService;
|
||||
final AssetCacheService _assetCacheService;
|
||||
final AlbumCacheService _albumCacheService;
|
||||
final SharedAlbumCacheService _sharedAlbumCacheService;
|
||||
|
||||
Future<bool> login(
|
||||
String email,
|
||||
@@ -153,7 +161,23 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
Future<bool> logout() async {
|
||||
Hive.box(userInfoBox).delete(accessTokenKey);
|
||||
state = state.copyWith(isAuthenticated: false);
|
||||
_assetCacheService.invalidate();
|
||||
_albumCacheService.invalidate();
|
||||
_sharedAlbumCacheService.invalidate();
|
||||
|
||||
// Remove login info from local storage
|
||||
var loginInfo =
|
||||
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
|
||||
if (loginInfo != null) {
|
||||
loginInfo.email = "";
|
||||
loginInfo.password = "";
|
||||
loginInfo.isSaveLogin = false;
|
||||
|
||||
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
|
||||
savedLoginInfoKey,
|
||||
loginInfo,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -199,5 +223,8 @@ final authenticationProvider =
|
||||
ref.watch(deviceInfoServiceProvider),
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(assetCacheServiceProvider),
|
||||
ref.watch(albumCacheServiceProvider),
|
||||
ref.watch(sharedAlbumCacheServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -228,7 +228,7 @@ class LoginButton extends ConsumerWidget {
|
||||
AutoRouter.of(context).push(const ChangePasswordRoute());
|
||||
} else {
|
||||
ref.watch(backupProvider.notifier).resumeBackup();
|
||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||
AutoRouter.of(context).replace(const TabControllerRoute());
|
||||
}
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -9,24 +10,50 @@ import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
||||
final AssetService _assetService;
|
||||
final AssetCacheService _assetCacheService;
|
||||
|
||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||
|
||||
AssetNotifier(this._assetService) : super([]);
|
||||
AssetNotifier(this._assetService, this._assetCacheService) : super([]);
|
||||
|
||||
_cacheState() {
|
||||
_assetCacheService.put(state);
|
||||
}
|
||||
|
||||
getAllAsset() async {
|
||||
final stopwatch = Stopwatch();
|
||||
|
||||
|
||||
if (await _assetCacheService.isValid() && state.isEmpty) {
|
||||
stopwatch.start();
|
||||
state = await _assetCacheService.get();
|
||||
debugPrint("Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
|
||||
stopwatch.reset();
|
||||
}
|
||||
|
||||
stopwatch.start();
|
||||
var allAssets = await _assetService.getAllAsset();
|
||||
debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
|
||||
stopwatch.reset();
|
||||
|
||||
if (allAssets != null) {
|
||||
state = allAssets;
|
||||
|
||||
stopwatch.start();
|
||||
_cacheState();
|
||||
debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
||||
stopwatch.reset();
|
||||
}
|
||||
}
|
||||
|
||||
clearAllAsset() {
|
||||
state = [];
|
||||
_cacheState();
|
||||
}
|
||||
|
||||
onNewAssetUploaded(AssetResponseDto newAsset) {
|
||||
state = [...state, newAsset];
|
||||
_cacheState();
|
||||
}
|
||||
|
||||
deleteAssets(Set<AssetResponseDto> deleteAssets) async {
|
||||
@@ -65,12 +92,15 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
||||
state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
||||
}
|
||||
}
|
||||
|
||||
_cacheState();
|
||||
}
|
||||
}
|
||||
|
||||
final assetProvider =
|
||||
StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
|
||||
return AssetNotifier(ref.watch(assetServiceProvider));
|
||||
return AssetNotifier(
|
||||
ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
|
||||
});
|
||||
|
||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
||||
|
||||
49
mobile/lib/shared/services/json_cache.dart
Normal file
49
mobile/lib/shared/services/json_cache.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
abstract class JsonCache<T> {
|
||||
final String cacheFileName;
|
||||
|
||||
JsonCache(this.cacheFileName);
|
||||
|
||||
Future<File> _getCacheFile() async {
|
||||
final basePath = await getTemporaryDirectory();
|
||||
final basePathName = basePath.path;
|
||||
|
||||
final file = File("$basePathName/$cacheFileName.bin");
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
Future<bool> isValid() async {
|
||||
final file = await _getCacheFile();
|
||||
return await file.exists();
|
||||
}
|
||||
|
||||
Future<void> invalidate() async {
|
||||
final file = await _getCacheFile();
|
||||
await file.delete();
|
||||
}
|
||||
|
||||
Future<void> putRawData(dynamic data) async {
|
||||
final jsonString = json.encode(data);
|
||||
final file = await _getCacheFile();
|
||||
|
||||
if (!await file.exists()) {
|
||||
await file.create();
|
||||
}
|
||||
|
||||
await file.writeAsString(jsonString);
|
||||
}
|
||||
|
||||
dynamic readRawData() async {
|
||||
final file = await _getCacheFile();
|
||||
final data = await file.readAsString();
|
||||
return json.decode(data);
|
||||
}
|
||||
|
||||
void put(T data);
|
||||
Future<T> get();
|
||||
}
|
||||
@@ -29,9 +29,9 @@ class SplashScreenPage extends HookConsumerWidget {
|
||||
if (isAuthenticated) {
|
||||
// Resume backup (if enable) then navigate
|
||||
ref.watch(backupProvider.notifier).resumeBackup();
|
||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||
AutoRouter.of(context).replace(const TabControllerRoute());
|
||||
} else {
|
||||
AutoRouter.of(context).push(const LoginRoute());
|
||||
AutoRouter.of(context).replace(const LoginRoute());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class SplashScreenPage extends HookConsumerWidget {
|
||||
if (loginInfo?.isSaveLogin == true) {
|
||||
performLoggingIn();
|
||||
} else {
|
||||
AutoRouter.of(context).push(const LoginRoute());
|
||||
AutoRouter.of(context).replace(const LoginRoute());
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -12,7 +12,6 @@ class TabControllerPage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||
|
||||
return AutoTabsRouter(
|
||||
routes: [
|
||||
const HomeRoute(),
|
||||
@@ -22,10 +21,16 @@ class TabControllerPage extends ConsumerWidget {
|
||||
],
|
||||
builder: (context, child, animation) {
|
||||
final tabsRouter = AutoTabsRouter.of(context);
|
||||
final appRouter = AutoRouter.of(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
tabsRouter.setActiveIndex(0);
|
||||
return false;
|
||||
bool atHomeTab = tabsRouter.activeIndex == 0;
|
||||
if (!atHomeTab) {
|
||||
tabsRouter.setActiveIndex(0);
|
||||
} else {
|
||||
appRouter.navigateBack();
|
||||
}
|
||||
return atHomeTab;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: FadeTransition(
|
||||
|
||||
@@ -19,6 +19,8 @@ doc/AssetTypeEnum.md
|
||||
doc/AuthenticationApi.md
|
||||
doc/CheckDuplicateAssetDto.md
|
||||
doc/CheckDuplicateAssetResponseDto.md
|
||||
doc/CheckExistingAssetsDto.md
|
||||
doc/CheckExistingAssetsResponseDto.md
|
||||
doc/CreateAlbumDto.md
|
||||
doc/CreateDeviceInfoDto.md
|
||||
doc/CreateProfileImageResponseDto.md
|
||||
@@ -48,6 +50,7 @@ doc/SearchAssetDto.md
|
||||
doc/ServerInfoApi.md
|
||||
doc/ServerInfoResponseDto.md
|
||||
doc/ServerPingResponse.md
|
||||
doc/ServerStatsResponseDto.md
|
||||
doc/ServerVersionReponseDto.md
|
||||
doc/SignUpDto.md
|
||||
doc/SmartInfoResponseDto.md
|
||||
@@ -56,6 +59,7 @@ doc/TimeGroupEnum.md
|
||||
doc/UpdateAlbumDto.md
|
||||
doc/UpdateDeviceInfoDto.md
|
||||
doc/UpdateUserDto.md
|
||||
doc/UsageByUserDto.md
|
||||
doc/UserApi.md
|
||||
doc/UserCountResponseDto.md
|
||||
doc/UserResponseDto.md
|
||||
@@ -91,6 +95,8 @@ lib/model/asset_response_dto.dart
|
||||
lib/model/asset_type_enum.dart
|
||||
lib/model/check_duplicate_asset_dto.dart
|
||||
lib/model/check_duplicate_asset_response_dto.dart
|
||||
lib/model/check_existing_assets_dto.dart
|
||||
lib/model/check_existing_assets_response_dto.dart
|
||||
lib/model/create_album_dto.dart
|
||||
lib/model/create_device_info_dto.dart
|
||||
lib/model/create_profile_image_response_dto.dart
|
||||
@@ -117,6 +123,7 @@ lib/model/remove_assets_dto.dart
|
||||
lib/model/search_asset_dto.dart
|
||||
lib/model/server_info_response_dto.dart
|
||||
lib/model/server_ping_response.dart
|
||||
lib/model/server_stats_response_dto.dart
|
||||
lib/model/server_version_reponse_dto.dart
|
||||
lib/model/sign_up_dto.dart
|
||||
lib/model/smart_info_response_dto.dart
|
||||
@@ -125,7 +132,10 @@ lib/model/time_group_enum.dart
|
||||
lib/model/update_album_dto.dart
|
||||
lib/model/update_device_info_dto.dart
|
||||
lib/model/update_user_dto.dart
|
||||
lib/model/usage_by_user_dto.dart
|
||||
lib/model/user_count_response_dto.dart
|
||||
lib/model/user_response_dto.dart
|
||||
lib/model/validate_access_token_response_dto.dart
|
||||
pubspec.yaml
|
||||
test/check_existing_assets_dto_test.dart
|
||||
test/check_existing_assets_response_dto_test.dart
|
||||
|
||||
@@ -76,6 +76,7 @@ Class | Method | HTTP request | Description
|
||||
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |
|
||||
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |
|
||||
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
|
||||
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
|
||||
*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset |
|
||||
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download |
|
||||
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
|
||||
@@ -102,6 +103,7 @@ Class | Method | HTTP request | Description
|
||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
|
||||
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
|
||||
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
|
||||
*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats |
|
||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image |
|
||||
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user |
|
||||
@@ -129,6 +131,8 @@ Class | Method | HTTP request | Description
|
||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||
- [CheckDuplicateAssetDto](doc//CheckDuplicateAssetDto.md)
|
||||
- [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md)
|
||||
- [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
|
||||
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
|
||||
- [CreateAlbumDto](doc//CreateAlbumDto.md)
|
||||
- [CreateDeviceInfoDto](doc//CreateDeviceInfoDto.md)
|
||||
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
|
||||
@@ -155,6 +159,7 @@ Class | Method | HTTP request | Description
|
||||
- [SearchAssetDto](doc//SearchAssetDto.md)
|
||||
- [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
|
||||
- [ServerPingResponse](doc//ServerPingResponse.md)
|
||||
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
||||
- [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
|
||||
- [SignUpDto](doc//SignUpDto.md)
|
||||
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
|
||||
@@ -163,6 +168,7 @@ Class | Method | HTTP request | Description
|
||||
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
||||
- [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
|
||||
- [UpdateUserDto](doc//UpdateUserDto.md)
|
||||
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||
- [UserCountResponseDto](doc//UserCountResponseDto.md)
|
||||
- [UserResponseDto](doc//UserResponseDto.md)
|
||||
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
|
||||
|
||||
@@ -10,6 +10,7 @@ All URIs are relative to */api*
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
|
||||
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
|
||||
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |
|
||||
[**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download |
|
||||
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
|
||||
@@ -76,6 +77,55 @@ 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)
|
||||
|
||||
# **checkExistingAssets**
|
||||
> CheckExistingAssetsResponseDto checkExistingAssets(checkExistingAssetsDto)
|
||||
|
||||
|
||||
|
||||
Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
|
||||
### 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();
|
||||
final checkExistingAssetsDto = CheckExistingAssetsDto(); // CheckExistingAssetsDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.checkExistingAssets(checkExistingAssetsDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AssetApi->checkExistingAssets: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**checkExistingAssetsDto** | [**CheckExistingAssetsDto**](CheckExistingAssetsDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**CheckExistingAssetsResponseDto**](CheckExistingAssetsResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **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)
|
||||
|
||||
# **deleteAsset**
|
||||
> List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto)
|
||||
|
||||
|
||||
16
mobile/openapi/doc/AssetCountResponseDto.md
Normal file
16
mobile/openapi/doc/AssetCountResponseDto.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# openapi.model.AssetCountResponseDto
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
16
mobile/openapi/doc/CheckExistingAssetsDto.md
Normal file
16
mobile/openapi/doc/CheckExistingAssetsDto.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# openapi.model.CheckExistingAssetsDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**deviceAssetIds** | **List<String>** | | [default to const []]
|
||||
**deviceId** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
15
mobile/openapi/doc/CheckExistingAssetsResponseDto.md
Normal file
15
mobile/openapi/doc/CheckExistingAssetsResponseDto.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# openapi.model.CheckExistingAssetsResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**existingIds** | **List<String>** | | [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**getServerInfo**](ServerInfoApi.md#getserverinfo) | **GET** /server-info |
|
||||
[**getServerVersion**](ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
|
||||
[**getStats**](ServerInfoApi.md#getstats) | **GET** /server-info/stats |
|
||||
[**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||
|
||||
|
||||
@@ -88,6 +89,43 @@ No authorization required
|
||||
|
||||
[[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)
|
||||
|
||||
# **getStats**
|
||||
> ServerStatsResponseDto getStats()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final api_instance = ServerInfoApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.getStats();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling ServerInfoApi->getStats: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**ServerStatsResponseDto**](ServerStatsResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### 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)
|
||||
|
||||
# **pingServer**
|
||||
> ServerPingResponse pingServer()
|
||||
|
||||
|
||||
20
mobile/openapi/doc/ServerStatsResponseDto.md
Normal file
20
mobile/openapi/doc/ServerStatsResponseDto.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# openapi.model.ServerStatsResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**photos** | **int** | |
|
||||
**videos** | **int** | |
|
||||
**objects** | **int** | |
|
||||
**usageRaw** | **int** | |
|
||||
**usage** | **String** | |
|
||||
**usageByUser** | [**List<UsageByUserDto>**](UsageByUserDto.md) | | [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
20
mobile/openapi/doc/UsageByUserDto.md
Normal file
20
mobile/openapi/doc/UsageByUserDto.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# openapi.model.UsageByUserDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**userId** | **String** | |
|
||||
**objects** | **int** | |
|
||||
**videos** | **int** | |
|
||||
**photos** | **int** | |
|
||||
**usageRaw** | **int** | |
|
||||
**usage** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ part 'model/asset_response_dto.dart';
|
||||
part 'model/asset_type_enum.dart';
|
||||
part 'model/check_duplicate_asset_dto.dart';
|
||||
part 'model/check_duplicate_asset_response_dto.dart';
|
||||
part 'model/check_existing_assets_dto.dart';
|
||||
part 'model/check_existing_assets_response_dto.dart';
|
||||
part 'model/create_album_dto.dart';
|
||||
part 'model/create_device_info_dto.dart';
|
||||
part 'model/create_profile_image_response_dto.dart';
|
||||
@@ -75,6 +77,7 @@ part 'model/remove_assets_dto.dart';
|
||||
part 'model/search_asset_dto.dart';
|
||||
part 'model/server_info_response_dto.dart';
|
||||
part 'model/server_ping_response.dart';
|
||||
part 'model/server_stats_response_dto.dart';
|
||||
part 'model/server_version_reponse_dto.dart';
|
||||
part 'model/sign_up_dto.dart';
|
||||
part 'model/smart_info_response_dto.dart';
|
||||
@@ -83,6 +86,7 @@ part 'model/time_group_enum.dart';
|
||||
part 'model/update_album_dto.dart';
|
||||
part 'model/update_device_info_dto.dart';
|
||||
part 'model/update_user_dto.dart';
|
||||
part 'model/usage_by_user_dto.dart';
|
||||
part 'model/user_count_response_dto.dart';
|
||||
part 'model/user_response_dto.dart';
|
||||
part 'model/validate_access_token_response_dto.dart';
|
||||
|
||||
@@ -72,6 +72,62 @@ class AssetApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [CheckExistingAssetsDto] checkExistingAssetsDto (required):
|
||||
Future<Response> checkExistingAssetsWithHttpInfo(CheckExistingAssetsDto checkExistingAssetsDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/asset/exist';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = checkExistingAssetsDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [CheckExistingAssetsDto] checkExistingAssetsDto (required):
|
||||
Future<CheckExistingAssetsResponseDto?> checkExistingAssets(CheckExistingAssetsDto checkExistingAssetsDto,) async {
|
||||
final response = await checkExistingAssetsWithHttpInfo(checkExistingAssetsDto,);
|
||||
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), 'CheckExistingAssetsResponseDto',) as CheckExistingAssetsResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /asset' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
@@ -98,6 +98,47 @@ class ServerInfoApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /server-info/stats' operation and returns the [Response].
|
||||
Future<Response> getStatsWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/server-info/stats';
|
||||
|
||||
// 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<ServerStatsResponseDto?> getStats() async {
|
||||
final response = await getStatsWithHttpInfo();
|
||||
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), 'ServerStatsResponseDto',) as ServerStatsResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /server-info/ping' operation and returns the [Response].
|
||||
Future<Response> pingServerWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
|
||||
@@ -220,6 +220,10 @@ class ApiClient {
|
||||
return CheckDuplicateAssetDto.fromJson(value);
|
||||
case 'CheckDuplicateAssetResponseDto':
|
||||
return CheckDuplicateAssetResponseDto.fromJson(value);
|
||||
case 'CheckExistingAssetsDto':
|
||||
return CheckExistingAssetsDto.fromJson(value);
|
||||
case 'CheckExistingAssetsResponseDto':
|
||||
return CheckExistingAssetsResponseDto.fromJson(value);
|
||||
case 'CreateAlbumDto':
|
||||
return CreateAlbumDto.fromJson(value);
|
||||
case 'CreateDeviceInfoDto':
|
||||
@@ -272,6 +276,8 @@ class ApiClient {
|
||||
return ServerInfoResponseDto.fromJson(value);
|
||||
case 'ServerPingResponse':
|
||||
return ServerPingResponse.fromJson(value);
|
||||
case 'ServerStatsResponseDto':
|
||||
return ServerStatsResponseDto.fromJson(value);
|
||||
case 'ServerVersionReponseDto':
|
||||
return ServerVersionReponseDto.fromJson(value);
|
||||
case 'SignUpDto':
|
||||
@@ -288,6 +294,8 @@ class ApiClient {
|
||||
return UpdateDeviceInfoDto.fromJson(value);
|
||||
case 'UpdateUserDto':
|
||||
return UpdateUserDto.fromJson(value);
|
||||
case 'UsageByUserDto':
|
||||
return UsageByUserDto.fromJson(value);
|
||||
case 'UserCountResponseDto':
|
||||
return UserCountResponseDto.fromJson(value);
|
||||
case 'UserResponseDto':
|
||||
|
||||
119
mobile/openapi/lib/model/asset_count_response_dto.dart
Normal file
119
mobile/openapi/lib/model/asset_count_response_dto.dart
Normal file
@@ -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 AssetCountResponseDto {
|
||||
/// Returns a new [AssetCountResponseDto] instance.
|
||||
AssetCountResponseDto({
|
||||
required this.photos,
|
||||
required this.videos,
|
||||
});
|
||||
|
||||
int photos;
|
||||
|
||||
int videos;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetCountResponseDto &&
|
||||
other.photos == photos &&
|
||||
other.videos == videos;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(photos.hashCode) +
|
||||
(videos.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetCountResponseDto[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 [AssetCountResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetCountResponseDto? 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 "AssetCountResponseDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "AssetCountResponseDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return AssetCountResponseDto(
|
||||
photos: mapValueOfType<int>(json, r'photos')!,
|
||||
videos: mapValueOfType<int>(json, r'videos')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetCountResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetCountResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetCountResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetCountResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetCountResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetCountResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetCountResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AssetCountResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetCountResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetCountResponseDto.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',
|
||||
};
|
||||
}
|
||||
|
||||
121
mobile/openapi/lib/model/check_existing_assets_dto.dart
Normal file
121
mobile/openapi/lib/model/check_existing_assets_dto.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// 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 CheckExistingAssetsDto {
|
||||
/// Returns a new [CheckExistingAssetsDto] instance.
|
||||
CheckExistingAssetsDto({
|
||||
this.deviceAssetIds = const [],
|
||||
required this.deviceId,
|
||||
});
|
||||
|
||||
List<String> deviceAssetIds;
|
||||
|
||||
String deviceId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsDto &&
|
||||
other.deviceAssetIds == deviceAssetIds &&
|
||||
other.deviceId == deviceId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(deviceAssetIds.hashCode) +
|
||||
(deviceId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CheckExistingAssetsDto[deviceAssetIds=$deviceAssetIds, deviceId=$deviceId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final _json = <String, dynamic>{};
|
||||
_json[r'deviceAssetIds'] = deviceAssetIds;
|
||||
_json[r'deviceId'] = deviceId;
|
||||
return _json;
|
||||
}
|
||||
|
||||
/// Returns a new [CheckExistingAssetsDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CheckExistingAssetsDto? 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 "CheckExistingAssetsDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "CheckExistingAssetsDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return CheckExistingAssetsDto(
|
||||
deviceAssetIds: json[r'deviceAssetIds'] is List
|
||||
? (json[r'deviceAssetIds'] as List).cast<String>()
|
||||
: const [],
|
||||
deviceId: mapValueOfType<String>(json, r'deviceId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<CheckExistingAssetsDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <CheckExistingAssetsDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = CheckExistingAssetsDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, CheckExistingAssetsDto> mapFromJson(dynamic json) {
|
||||
final map = <String, CheckExistingAssetsDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CheckExistingAssetsDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of CheckExistingAssetsDto-objects as value to a dart map
|
||||
static Map<String, List<CheckExistingAssetsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<CheckExistingAssetsDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CheckExistingAssetsDto.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>{
|
||||
'deviceAssetIds',
|
||||
'deviceId',
|
||||
};
|
||||
}
|
||||
|
||||
113
mobile/openapi/lib/model/check_existing_assets_response_dto.dart
Normal file
113
mobile/openapi/lib/model/check_existing_assets_response_dto.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// 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 CheckExistingAssetsResponseDto {
|
||||
/// Returns a new [CheckExistingAssetsResponseDto] instance.
|
||||
CheckExistingAssetsResponseDto({
|
||||
this.existingIds = const [],
|
||||
});
|
||||
|
||||
List<String> existingIds;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsResponseDto &&
|
||||
other.existingIds == existingIds;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(existingIds.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CheckExistingAssetsResponseDto[existingIds=$existingIds]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final _json = <String, dynamic>{};
|
||||
_json[r'existingIds'] = existingIds;
|
||||
return _json;
|
||||
}
|
||||
|
||||
/// Returns a new [CheckExistingAssetsResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CheckExistingAssetsResponseDto? 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 "CheckExistingAssetsResponseDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "CheckExistingAssetsResponseDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return CheckExistingAssetsResponseDto(
|
||||
existingIds: json[r'existingIds'] is List
|
||||
? (json[r'existingIds'] as List).cast<String>()
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<CheckExistingAssetsResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <CheckExistingAssetsResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = CheckExistingAssetsResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, CheckExistingAssetsResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, CheckExistingAssetsResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CheckExistingAssetsResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of CheckExistingAssetsResponseDto-objects as value to a dart map
|
||||
static Map<String, List<CheckExistingAssetsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<CheckExistingAssetsResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CheckExistingAssetsResponseDto.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>{
|
||||
'existingIds',
|
||||
};
|
||||
}
|
||||
|
||||
151
mobile/openapi/lib/model/server_stats_response_dto.dart
Normal file
151
mobile/openapi/lib/model/server_stats_response_dto.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// 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 ServerStatsResponseDto {
|
||||
/// Returns a new [ServerStatsResponseDto] instance.
|
||||
ServerStatsResponseDto({
|
||||
required this.photos,
|
||||
required this.videos,
|
||||
required this.objects,
|
||||
required this.usageRaw,
|
||||
required this.usage,
|
||||
this.usageByUser = const [],
|
||||
});
|
||||
|
||||
int photos;
|
||||
|
||||
int videos;
|
||||
|
||||
int objects;
|
||||
|
||||
int usageRaw;
|
||||
|
||||
String usage;
|
||||
|
||||
List<UsageByUserDto> usageByUser;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ServerStatsResponseDto &&
|
||||
other.photos == photos &&
|
||||
other.videos == videos &&
|
||||
other.objects == objects &&
|
||||
other.usageRaw == usageRaw &&
|
||||
other.usage == usage &&
|
||||
other.usageByUser == usageByUser;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(photos.hashCode) +
|
||||
(videos.hashCode) +
|
||||
(objects.hashCode) +
|
||||
(usageRaw.hashCode) +
|
||||
(usage.hashCode) +
|
||||
(usageByUser.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ServerStatsResponseDto[photos=$photos, videos=$videos, objects=$objects, usageRaw=$usageRaw, usage=$usage, usageByUser=$usageByUser]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final _json = <String, dynamic>{};
|
||||
_json[r'photos'] = photos;
|
||||
_json[r'videos'] = videos;
|
||||
_json[r'objects'] = objects;
|
||||
_json[r'usageRaw'] = usageRaw;
|
||||
_json[r'usage'] = usage;
|
||||
_json[r'usageByUser'] = usageByUser;
|
||||
return _json;
|
||||
}
|
||||
|
||||
/// Returns a new [ServerStatsResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ServerStatsResponseDto? 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 "ServerStatsResponseDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "ServerStatsResponseDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return ServerStatsResponseDto(
|
||||
photos: mapValueOfType<int>(json, r'photos')!,
|
||||
videos: mapValueOfType<int>(json, r'videos')!,
|
||||
objects: mapValueOfType<int>(json, r'objects')!,
|
||||
usageRaw: mapValueOfType<int>(json, r'usageRaw')!,
|
||||
usage: mapValueOfType<String>(json, r'usage')!,
|
||||
usageByUser: UsageByUserDto.listFromJson(json[r'usageByUser'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<ServerStatsResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ServerStatsResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ServerStatsResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, ServerStatsResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, ServerStatsResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = ServerStatsResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of ServerStatsResponseDto-objects as value to a dart map
|
||||
static Map<String, List<ServerStatsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<ServerStatsResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = ServerStatsResponseDto.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',
|
||||
'objects',
|
||||
'usageRaw',
|
||||
'usage',
|
||||
'usageByUser',
|
||||
};
|
||||
}
|
||||
|
||||
151
mobile/openapi/lib/model/usage_by_user_dto.dart
Normal file
151
mobile/openapi/lib/model/usage_by_user_dto.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// 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 UsageByUserDto {
|
||||
/// Returns a new [UsageByUserDto] instance.
|
||||
UsageByUserDto({
|
||||
required this.userId,
|
||||
required this.objects,
|
||||
required this.videos,
|
||||
required this.photos,
|
||||
required this.usageRaw,
|
||||
required this.usage,
|
||||
});
|
||||
|
||||
String userId;
|
||||
|
||||
int objects;
|
||||
|
||||
int videos;
|
||||
|
||||
int photos;
|
||||
|
||||
int usageRaw;
|
||||
|
||||
String usage;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UsageByUserDto &&
|
||||
other.userId == userId &&
|
||||
other.objects == objects &&
|
||||
other.videos == videos &&
|
||||
other.photos == photos &&
|
||||
other.usageRaw == usageRaw &&
|
||||
other.usage == usage;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(userId.hashCode) +
|
||||
(objects.hashCode) +
|
||||
(videos.hashCode) +
|
||||
(photos.hashCode) +
|
||||
(usageRaw.hashCode) +
|
||||
(usage.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UsageByUserDto[userId=$userId, objects=$objects, videos=$videos, photos=$photos, usageRaw=$usageRaw, usage=$usage]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final _json = <String, dynamic>{};
|
||||
_json[r'userId'] = userId;
|
||||
_json[r'objects'] = objects;
|
||||
_json[r'videos'] = videos;
|
||||
_json[r'photos'] = photos;
|
||||
_json[r'usageRaw'] = usageRaw;
|
||||
_json[r'usage'] = usage;
|
||||
return _json;
|
||||
}
|
||||
|
||||
/// Returns a new [UsageByUserDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static UsageByUserDto? 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 "UsageByUserDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "UsageByUserDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return UsageByUserDto(
|
||||
userId: mapValueOfType<String>(json, r'userId')!,
|
||||
objects: mapValueOfType<int>(json, r'objects')!,
|
||||
videos: mapValueOfType<int>(json, r'videos')!,
|
||||
photos: mapValueOfType<int>(json, r'photos')!,
|
||||
usageRaw: mapValueOfType<int>(json, r'usageRaw')!,
|
||||
usage: mapValueOfType<String>(json, r'usage')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<UsageByUserDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UsageByUserDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UsageByUserDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, UsageByUserDto> mapFromJson(dynamic json) {
|
||||
final map = <String, UsageByUserDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UsageByUserDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of UsageByUserDto-objects as value to a dart map
|
||||
static Map<String, List<UsageByUserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<UsageByUserDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UsageByUserDto.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>{
|
||||
'userId',
|
||||
'objects',
|
||||
'videos',
|
||||
'photos',
|
||||
'usageRaw',
|
||||
'usage',
|
||||
};
|
||||
}
|
||||
|
||||
32
mobile/openapi/test/asset_count_response_dto_test.dart
Normal file
32
mobile/openapi/test/asset_count_response_dto_test.dart
Normal file
@@ -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 AssetCountResponseDto
|
||||
void main() {
|
||||
// final instance = AssetCountResponseDto();
|
||||
|
||||
group('test AssetCountResponseDto', () {
|
||||
// int photos
|
||||
test('to test the property `photos`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int videos
|
||||
test('to test the property `videos`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
32
mobile/openapi/test/check_existing_assets_dto_test.dart
Normal file
32
mobile/openapi/test/check_existing_assets_dto_test.dart
Normal file
@@ -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 CheckExistingAssetsDto
|
||||
void main() {
|
||||
// final instance = CheckExistingAssetsDto();
|
||||
|
||||
group('test CheckExistingAssetsDto', () {
|
||||
// List<String> deviceAssetIds (default value: const [])
|
||||
test('to test the property `deviceAssetIds`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String deviceId
|
||||
test('to test the property `deviceId`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// 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 CheckExistingAssetsResponseDto
|
||||
void main() {
|
||||
// final instance = CheckExistingAssetsResponseDto();
|
||||
|
||||
group('test CheckExistingAssetsResponseDto', () {
|
||||
// List<String> existingIds (default value: const [])
|
||||
test('to test the property `existingIds`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
42
mobile/openapi/test/server_stats_response_dto_test.dart
Normal file
42
mobile/openapi/test/server_stats_response_dto_test.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// 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 ServerStatsResponseDto
|
||||
void main() {
|
||||
// final instance = ServerStatsResponseDto();
|
||||
|
||||
group('test ServerStatsResponseDto', () {
|
||||
// int photos
|
||||
test('to test the property `photos`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int videos
|
||||
test('to test the property `videos`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int objects
|
||||
test('to test the property `objects`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// UsagePerUser diskUsagesByUser
|
||||
test('to test the property `diskUsagesByUser`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
42
mobile/openapi/test/usage_by_user_dto_test.dart
Normal file
42
mobile/openapi/test/usage_by_user_dto_test.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// 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 UsageByUserDto
|
||||
void main() {
|
||||
// final instance = UsageByUserDto();
|
||||
|
||||
group('test UsageByUserDto', () {
|
||||
// int usageRaw
|
||||
test('to test the property `usageRaw`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// num objects
|
||||
test('to test the property `objects`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// num videos
|
||||
test('to test the property `videos`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// num images
|
||||
test('to test the property `images`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.32.0+50
|
||||
version: 1.33.0+52
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
FROM docker.io/nginxinc/nginx-unprivileged:latest
|
||||
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
COPY nginx.conf "/etc/nginx/nginx.conf"
|
||||
|
||||
CMD nginx -g "daemon off;"
|
||||
21
nginx/LICENSE
Normal file
21
nginx/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Hau Tran
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
upload/
|
||||
dist/
|
||||
coverage/
|
||||
.reverse-geocoding-dump
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Build stage
|
||||
FROM node:16-alpine3.14 as builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
@@ -12,16 +11,18 @@ COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# Prod stage
|
||||
FROM node:16-alpine3.14
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY start-server.sh start-microservices.sh ./
|
||||
|
||||
RUN mkdir -p /usr/src/app/dist \
|
||||
&& mkdir /usr/src/app/.reverse-geocoding-dump \
|
||||
&& apk add --no-cache libheif vips ffmpeg
|
||||
|
||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||
@@ -29,6 +30,11 @@ COPY --from=builder /usr/src/app/dist ./dist
|
||||
|
||||
RUN npm prune --production
|
||||
|
||||
RUN chown -R node:0 /usr/src/app \
|
||||
&& chmod -R g=u /usr/src/app
|
||||
|
||||
RUN addgroup node root
|
||||
|
||||
VOLUME /usr/src/app/upload
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
21
server/LICENSE
Normal file
21
server/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Hau Tran
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -137,6 +137,7 @@ describe('Album service', () => {
|
||||
getAssetWithNoEXIF: jest.fn(),
|
||||
getAssetWithNoThumbnail: jest.fn(),
|
||||
getAssetWithNoSmartInfo: jest.fn(),
|
||||
getExistingAssets: jest.fn(),
|
||||
};
|
||||
|
||||
sut = new AlbumService(albumRepositoryMock, assetRepositoryMock);
|
||||
|
||||
@@ -10,6 +10,9 @@ import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group
|
||||
import { TimeGroupEnum } from './dto/get-asset-count-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 { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||
import { In } from 'typeorm/find-options/operator/In';
|
||||
|
||||
export interface IAssetRepository {
|
||||
create(
|
||||
@@ -32,6 +35,7 @@ export interface IAssetRepository {
|
||||
getAssetWithNoThumbnail(): Promise<AssetEntity[]>;
|
||||
getAssetWithNoEXIF(): Promise<AssetEntity[]>;
|
||||
getAssetWithNoSmartInfo(): Promise<AssetEntity[]>;
|
||||
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<CheckExistingAssetsResponseDto>;
|
||||
}
|
||||
|
||||
export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
|
||||
@@ -279,4 +283,17 @@ export class AssetRepository implements IAssetRepository {
|
||||
relations: ['exifInfo'],
|
||||
});
|
||||
}
|
||||
|
||||
async getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<CheckExistingAssetsResponseDto> {
|
||||
const existingAssets = await this.assetRepository.find({
|
||||
select: {deviceAssetId: true},
|
||||
where: {
|
||||
deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds),
|
||||
deviceId: checkDuplicateAssetDto.deviceId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
return new CheckExistingAssetsResponseDto(existingAssets.map(a => a.deviceAssetId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-buck
|
||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@@ -74,6 +76,7 @@ export class AssetController {
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
||||
@Response({ passthrough: true }) res: Res,
|
||||
): Promise<AssetFileUploadResponseDto> {
|
||||
const checksum = await this.assetService.calculateChecksum(file.path);
|
||||
|
||||
@@ -111,6 +114,7 @@ export class AssetController {
|
||||
|
||||
if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') {
|
||||
const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum);
|
||||
res.status(200); // normal POST is 201. we use 200 to indicate the asset already exists
|
||||
return new AssetFileUploadResponseDto(existedAsset.id);
|
||||
}
|
||||
|
||||
@@ -182,6 +186,7 @@ export class AssetController {
|
||||
async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||
return this.assetService.getAssetCountByUserId(authUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all AssetEntity belong to the user
|
||||
*/
|
||||
@@ -253,4 +258,16 @@ export class AssetController {
|
||||
): Promise<CheckDuplicateAssetResponseDto> {
|
||||
return await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
*/
|
||||
@Post('/exist')
|
||||
@HttpCode(200)
|
||||
async checkExistingAssets(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(ValidationPipe) checkExistingAssetsDto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ describe('AssetService', () => {
|
||||
getAssetWithNoEXIF: jest.fn(),
|
||||
getAssetWithNoThumbnail: jest.fn(),
|
||||
getAssetWithNoSmartInfo: jest.fn(),
|
||||
getExistingAssets: jest.fn(),
|
||||
};
|
||||
|
||||
sui = new AssetService(assetRepositoryMock, a);
|
||||
|
||||
@@ -37,6 +37,8 @@ import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-buck
|
||||
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';
|
||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
|
||||
@@ -466,6 +468,13 @@ export class AssetService {
|
||||
return new CheckDuplicateAssetResponseDto(isDuplicated, res?.id);
|
||||
}
|
||||
|
||||
async checkExistingAssets(
|
||||
authUser: AuthUserDto,
|
||||
checkExistingAssetsDto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
return this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto);
|
||||
}
|
||||
|
||||
async getAssetCountByTimeBucket(
|
||||
authUser: AuthUserDto,
|
||||
getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class CheckExistingAssetsDto {
|
||||
@IsNotEmpty()
|
||||
deviceAssetIds!: string[];
|
||||
|
||||
@IsNotEmpty()
|
||||
deviceId!: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class CheckExistingAssetsResponseDto {
|
||||
constructor(existingIds: string[]) {
|
||||
this.existingIds = existingIds;
|
||||
}
|
||||
existingIds: string[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Post, Res, UseGuards, ValidationPipe } from '@nestjs/common';
|
||||
import { Body, Controller, Post, Res, UseGuards, ValidationPipe, Ip } from '@nestjs/common';
|
||||
import { ApiBadRequestResponse, ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
@@ -19,9 +19,10 @@ export class AuthController {
|
||||
@Post('/login')
|
||||
async login(
|
||||
@Body(new ValidationPipe({ transform: true })) loginCredential: LoginCredentialDto,
|
||||
@Ip() clientIp: string,
|
||||
@Res() response: Response,
|
||||
): Promise<LoginResponseDto> {
|
||||
const loginResponse = await this.authService.login(loginCredential);
|
||||
const loginResponse = await this.authService.login(loginCredential, clientIp);
|
||||
|
||||
// Set Cookies
|
||||
const accessTokenCookie = this.authService.getCookieWithJwtToken(loginResponse);
|
||||
|
||||
@@ -50,10 +50,11 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async login(loginCredential: LoginCredentialDto): Promise<LoginResponseDto> {
|
||||
public async login(loginCredential: LoginCredentialDto, clientIp: string): Promise<LoginResponseDto> {
|
||||
const validatedUser = await this.validateUser(loginCredential);
|
||||
|
||||
if (!validatedUser) {
|
||||
Logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`);
|
||||
throw new BadRequestException('Incorrect email or password');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ApiResponseProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LogoutResponseDto {
|
||||
constructor (successful: boolean) {
|
||||
this.successful = successful;
|
||||
}
|
||||
constructor(successful: boolean) {
|
||||
this.successful = successful;
|
||||
}
|
||||
|
||||
@ApiResponseProperty()
|
||||
successful!: boolean;
|
||||
};
|
||||
@ApiResponseProperty()
|
||||
successful!: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { UsageByUserDto } from './usage-by-user-response.dto';
|
||||
|
||||
export class ServerStatsResponseDto {
|
||||
constructor() {
|
||||
this.photos = 0;
|
||||
this.videos = 0;
|
||||
this.objects = 0;
|
||||
this.usageByUser = [];
|
||||
this.usageRaw = 0;
|
||||
this.usage = '';
|
||||
}
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
objects!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usageRaw!: number;
|
||||
|
||||
@ApiProperty({ type: 'string' })
|
||||
usage!: string;
|
||||
|
||||
@ApiProperty({
|
||||
isArray: true,
|
||||
type: UsageByUserDto,
|
||||
title: 'Array of usage for each user',
|
||||
example: [
|
||||
{
|
||||
photos: 1,
|
||||
videos: 1,
|
||||
objects: 1,
|
||||
diskUsageRaw: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
usageByUser!: UsageByUserDto[];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UsageByUserDto {
|
||||
constructor(userId: string) {
|
||||
this.userId = userId;
|
||||
this.objects = 0;
|
||||
this.videos = 0;
|
||||
this.photos = 0;
|
||||
}
|
||||
|
||||
@ApiProperty({ type: 'string' })
|
||||
userId: string;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
objects: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos: number;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usageRaw!: number;
|
||||
@ApiProperty({ type: 'string' })
|
||||
usage!: string;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
import { serverVersion } from '../../constants/server_version.constant';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
|
||||
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
|
||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
|
||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
import { AdminRolesGuard } from '../../middlewares/admin-role-guard.middleware';
|
||||
|
||||
@ApiTags('Server Info')
|
||||
@Controller('server-info')
|
||||
@@ -25,4 +28,11 @@ export class ServerInfoController {
|
||||
async getServerVersion(): Promise<ServerVersionReponseDto> {
|
||||
return serverVersion;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AdminRolesGuard)
|
||||
@Get('/stats')
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
return await this.serverInfoService.getStats();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
import { ServerInfoController } from './server-info.controller';
|
||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
|
||||
import { UserEntity } from '@app/database/entities/user.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AssetEntity, UserEntity]), ImmichJwtModule],
|
||||
controllers: [ServerInfoController],
|
||||
providers: [ServerInfoService],
|
||||
})
|
||||
|
||||
@@ -2,9 +2,21 @@ import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||
import diskusage from 'diskusage';
|
||||
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
|
||||
import { UsageByUserDto } from './response-dto/usage-by-user-response.dto';
|
||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import path from 'path';
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
|
||||
@Injectable()
|
||||
export class ServerInfoService {
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity)
|
||||
private assetRepository: Repository<AssetEntity>,
|
||||
) {}
|
||||
|
||||
async getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
const diskInfo = await diskusage.check(APP_UPLOAD_LOCATION);
|
||||
|
||||
@@ -18,7 +30,6 @@ export class ServerInfoService {
|
||||
serverInfo.diskSizeRaw = diskInfo.total;
|
||||
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
||||
serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
@@ -48,4 +59,61 @@ export class ServerInfoService {
|
||||
return `${sizeInByte}B`;
|
||||
}
|
||||
}
|
||||
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
const res = await this.assetRepository
|
||||
.createQueryBuilder('asset')
|
||||
.select(`COUNT(asset.id)`, 'count')
|
||||
.addSelect(`asset.type`, 'type')
|
||||
.addSelect(`asset.userId`, 'userId')
|
||||
.groupBy('asset.type, asset.userId')
|
||||
.addGroupBy('asset.type')
|
||||
.getRawMany();
|
||||
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
const tmpMap = new Map<string, UsageByUserDto>();
|
||||
const getUsageByUser = (id: string) => tmpMap.get(id) || new UsageByUserDto(id);
|
||||
res.map((item) => {
|
||||
const usage: UsageByUserDto = getUsageByUser(item.userId);
|
||||
if (item.type === 'IMAGE') {
|
||||
usage.photos = parseInt(item.count);
|
||||
serverStats.photos += usage.photos;
|
||||
} else if (item.type === 'VIDEO') {
|
||||
usage.videos = parseInt(item.count);
|
||||
serverStats.videos += usage.videos;
|
||||
}
|
||||
tmpMap.set(item.userId, usage);
|
||||
});
|
||||
|
||||
for (const userId of tmpMap.keys()) {
|
||||
const usage = getUsageByUser(userId);
|
||||
const userDiskUsage = await ServerInfoService.getDirectoryStats(path.join(APP_UPLOAD_LOCATION, userId));
|
||||
usage.usageRaw = userDiskUsage.size;
|
||||
usage.objects = userDiskUsage.fileCount;
|
||||
usage.usage = ServerInfoService.getHumanReadableString(usage.usageRaw);
|
||||
serverStats.usageRaw += usage.usageRaw;
|
||||
serverStats.objects += usage.objects;
|
||||
}
|
||||
serverStats.usage = ServerInfoService.getHumanReadableString(serverStats.usageRaw);
|
||||
serverStats.usageByUser = Array.from(tmpMap.values());
|
||||
return serverStats;
|
||||
}
|
||||
|
||||
private static async getDirectoryStats(dirPath: string) {
|
||||
let size = 0;
|
||||
let fileCount = 0;
|
||||
for (const filename of readdirSync(dirPath)) {
|
||||
const absFilename = path.join(dirPath, filename);
|
||||
const fileStat = statSync(absFilename);
|
||||
if (fileStat.isFile()) {
|
||||
size += fileStat.size;
|
||||
fileCount += 1;
|
||||
} else if (fileStat.isDirectory()) {
|
||||
const subDirStat = await ServerInfoService.getDirectoryStats(absFilename);
|
||||
size += subDirStat.size;
|
||||
fileCount += subDirStat.fileCount;
|
||||
}
|
||||
}
|
||||
return { size, fileCount };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import { validate } from 'class-validator';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
|
||||
describe('create user DTO', () => {
|
||||
it('validates the email', async() => {
|
||||
it('validates the email', async () => {
|
||||
const params: Partial<CreateUserDto> = {
|
||||
email: undefined,
|
||||
password: 'password',
|
||||
firstName: 'first name',
|
||||
lastName: 'last name',
|
||||
}
|
||||
};
|
||||
let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
|
||||
let errors = await validate(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Not, Repository } from 'typeorm';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { UpdateUserDto } from './dto/update-user.dto'
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
|
||||
export interface IUserRepository {
|
||||
get(userId: string): Promise<UserEntity | null>;
|
||||
@@ -92,4 +92,4 @@ export class UserRepository implements IUserRepository {
|
||||
user.profileImagePath = fileInfo.path;
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import { UserRepository, USER_REPOSITORY } from './user-repository';
|
||||
ImmichJwtService,
|
||||
{
|
||||
provide: USER_REPOSITORY,
|
||||
useClass: UserRepository
|
||||
}
|
||||
useClass: UserRepository,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class UserModule {}
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface IServerVersion {
|
||||
|
||||
export const serverVersion: IServerVersion = {
|
||||
major: 1,
|
||||
minor: 32,
|
||||
minor: 33,
|
||||
patch: 0,
|
||||
build: 50,
|
||||
build: 52,
|
||||
};
|
||||
|
||||
@@ -65,7 +65,11 @@ export class ThumbnailGeneratorProcessor {
|
||||
|
||||
if (asset.type == AssetType.IMAGE) {
|
||||
try {
|
||||
await sharp(asset.originalPath).resize(1440, 2560, { fit: 'inside' }).jpeg().rotate().toFile(jpegThumbnailPath);
|
||||
await sharp(asset.originalPath, { failOnError: false })
|
||||
.resize(1440, 2560, { fit: 'inside' })
|
||||
.jpeg()
|
||||
.rotate()
|
||||
.toFile(jpegThumbnailPath);
|
||||
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||
} catch (error) {
|
||||
Logger.error('Failed to generate jpeg thumbnail for asset: ' + asset.id);
|
||||
@@ -135,7 +139,7 @@ export class ThumbnailGeneratorProcessor {
|
||||
const webpPath = asset.resizePath.replace('jpeg', 'webp');
|
||||
|
||||
try {
|
||||
await sharp(asset.resizePath).resize(250).webp().rotate().toFile(webpPath);
|
||||
await sharp(asset.resizePath, { failOnError: false }).resize(250).webp().rotate().toFile(webpPath);
|
||||
await this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
|
||||
} catch (error) {
|
||||
Logger.error('Failed to generate webp thumbnail for asset: ' + asset.id);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,20 +1,20 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ConfigModuleOptions } from '@nestjs/config';
|
||||
import Joi from 'joi';
|
||||
import { createSecretKey, generateKeySync } from 'node:crypto'
|
||||
import { createSecretKey, generateKeySync } from 'node:crypto';
|
||||
|
||||
const jwtSecretValidator: Joi.CustomValidator<string> = (value, ) => {
|
||||
const key = createSecretKey(value, "base64")
|
||||
const keySizeBits = (key.symmetricKeySize ?? 0) * 8
|
||||
const jwtSecretValidator: Joi.CustomValidator<string> = (value) => {
|
||||
const key = createSecretKey(value, 'base64');
|
||||
const keySizeBits = (key.symmetricKeySize ?? 0) * 8;
|
||||
|
||||
if (keySizeBits < 128) {
|
||||
const newKey = generateKeySync('hmac', { length: 256 }).export().toString('base64')
|
||||
Logger.warn("The current JWT_SECRET key is insecure. It should be at least 128 bits long!")
|
||||
Logger.warn(`Here is a new, securely generated key that you can use instead: ${newKey}`)
|
||||
const newKey = generateKeySync('hmac', { length: 256 }).export().toString('base64');
|
||||
Logger.warn('The current JWT_SECRET key is insecure. It should be at least 128 bits long!');
|
||||
Logger.warn(`Here is a new, securely generated key that you can use instead: ${newKey}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
export const immichAppConfig: ConfigModuleOptions = {
|
||||
envFilePath: '.env',
|
||||
@@ -26,7 +26,7 @@ export const immichAppConfig: ConfigModuleOptions = {
|
||||
DB_DATABASE_NAME: Joi.string().required(),
|
||||
JWT_SECRET: Joi.string().required().custom(jwtSecretValidator),
|
||||
DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false),
|
||||
REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0,1,2,3).default(3),
|
||||
REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0, 1, 2, 3).default(3),
|
||||
LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ export class AlbumEntity {
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: string;
|
||||
|
||||
@Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true})
|
||||
@Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true })
|
||||
albumThumbnailAssetId!: string | null;
|
||||
|
||||
@OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique} from 'typeorm';
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import { AlbumEntity } from './album.entity';
|
||||
import { AssetEntity } from './asset.entity';
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RenameAssetAlbumIdSequence1656888591977 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`alter sequence asset_shared_album_id_seq rename to asset_album_id_seq;`);
|
||||
await queryRunner.query(
|
||||
`alter table asset_album alter column id set default nextval('asset_album_id_seq'::regclass);`,
|
||||
);
|
||||
}
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`alter sequence asset_shared_album_id_seq rename to asset_album_id_seq;`);
|
||||
await queryRunner.query(`alter table asset_album alter column id set default nextval('asset_album_id_seq'::regclass);`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`alter sequence asset_album_id_seq rename to asset_shared_album_id_seq;`);
|
||||
await queryRunner.query(`alter table asset_album alter column id set default nextval('asset_shared_album_id_seq'::regclass);`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`alter sequence asset_album_id_seq rename to asset_shared_album_id_seq;`);
|
||||
await queryRunner.query(
|
||||
`alter table asset_album alter column id set default nextval('asset_shared_album_id_seq'::regclass);`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class DropExifTextSearchableColumns1656888918620 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exif_text_searchable_column"`);
|
||||
}
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exif_text_searchable_column"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE exif
|
||||
DROP COLUMN IF EXISTS exif_text_searchable_column;
|
||||
|
||||
@@ -29,6 +28,5 @@ export class DropExifTextSearchableColumns1656888918620 implements MigrationInte
|
||||
ON exif
|
||||
USING GIN (exif_text_searchable_column);
|
||||
`);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class MatchMigrationsWithTypeORMEntities1656889061566 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
COALESCE(orientation, '') || ' ' ||
|
||||
@@ -11,36 +10,63 @@ export class MatchMigrationsWithTypeORMEntities1656889061566 implements Migratio
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", ''))) STORED`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
|
||||
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","exifTextSearchableColumn","postgres","public","exif"]);
|
||||
await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["postgres","public","exif","GENERATED_COLUMN","exifTextSearchableColumn","TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))"]);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_unique_asset_in_album" UNIQUE ("albumId", "assetId")`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_256a30a03a4a0aff0394051397d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_7ae4e03729895bf87e056d7b598" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'postgres', 'public', 'exif'],
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
'postgres',
|
||||
'public',
|
||||
'exif',
|
||||
'GENERATED_COLUMN',
|
||||
'exifTextSearchableColumn',
|
||||
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
|
||||
],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_unique_asset_in_album" UNIQUE ("albumId", "assetId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_256a30a03a4a0aff0394051397d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_7ae4e03729895bf87e056d7b598" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" DROP NOT NULL`);
|
||||
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","exifTextSearchableColumn","immich","public","exif"]);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_unique_asset_in_album"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288" UNIQUE ("albumId", "assetId")`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
}
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "shouldChangePassword" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "profileImagePath" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" DROP NOT NULL`);
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_unique_asset_in_album"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288" UNIQUE ("albumId", "assetId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddAssetChecksum1661881837496 implements MigrationInterface {
|
||||
name = 'AddAssetChecksum1661881837496'
|
||||
name = 'AddAssetChecksum1661881837496';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD "checksum" bytea`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE 'checksum' IS NOT NULL`);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE 'checksum' IS NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateAssetTableWithNewUniqueConstraint1661971370662 implements MigrationInterface {
|
||||
name = 'UpdateAssetTableWithNewUniqueConstraint1661971370662'
|
||||
name = 'UpdateAssetTableWithNewUniqueConstraint1661971370662';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e"`);
|
||||
@@ -10,7 +10,8 @@ export class UpdateAssetTableWithNewUniqueConstraint1661971370662 implements Mig
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_userid_checksum"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e" UNIQUE ("deviceAssetId", "userId", "deviceId")`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "assets" ADD CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e" UNIQUE ("deviceAssetId", "userId", "deviceId")`,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Our Node base image
|
||||
FROM node:16-alpine3.14 as base
|
||||
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN chown node:node /usr/src/app
|
||||
|
||||
RUN apk add --no-cache setpriv
|
||||
RUN chown node:node /usr/src/app && \
|
||||
apk add --no-cache setpriv
|
||||
|
||||
COPY --chown=node:node package*.json ./
|
||||
|
||||
@@ -13,7 +15,11 @@ RUN npm ci
|
||||
|
||||
COPY --chown=node:node . .
|
||||
|
||||
RUN npm run build
|
||||
RUN npm run build \
|
||||
&& chown -R node:0 /usr/src/app \
|
||||
&& chmod -R g=u /usr/src/app
|
||||
|
||||
RUN addgroup node root
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
21
web/LICENSE
Normal file
21
web/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Hau Tran
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -452,6 +452,38 @@ export interface CheckDuplicateAssetResponseDto {
|
||||
*/
|
||||
'id'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface CheckExistingAssetsDto
|
||||
*/
|
||||
export interface CheckExistingAssetsDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof CheckExistingAssetsDto
|
||||
*/
|
||||
'deviceAssetIds': Array<string>;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof CheckExistingAssetsDto
|
||||
*/
|
||||
'deviceId': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface CheckExistingAssetsResponseDto
|
||||
*/
|
||||
export interface CheckExistingAssetsResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof CheckExistingAssetsResponseDto
|
||||
*/
|
||||
'existingIds': Array<string>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -1157,6 +1189,49 @@ export interface ServerPingResponse {
|
||||
*/
|
||||
'res': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ServerStatsResponseDto
|
||||
*/
|
||||
export interface ServerStatsResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ServerStatsResponseDto
|
||||
*/
|
||||
'photos': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ServerStatsResponseDto
|
||||
*/
|
||||
'videos': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ServerStatsResponseDto
|
||||
*/
|
||||
'objects': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ServerStatsResponseDto
|
||||
*/
|
||||
'usageRaw': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ServerStatsResponseDto
|
||||
*/
|
||||
'usage': string;
|
||||
/**
|
||||
*
|
||||
* @type {Array<UsageByUserDto>}
|
||||
* @memberof ServerStatsResponseDto
|
||||
*/
|
||||
'usageByUser': Array<UsageByUserDto>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -1365,6 +1440,49 @@ export interface UpdateUserDto {
|
||||
*/
|
||||
'profileImagePath'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UsageByUserDto
|
||||
*/
|
||||
export interface UsageByUserDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UsageByUserDto
|
||||
*/
|
||||
'userId': string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof UsageByUserDto
|
||||
*/
|
||||
'objects': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof UsageByUserDto
|
||||
*/
|
||||
'videos': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof UsageByUserDto
|
||||
*/
|
||||
'photos': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof UsageByUserDto
|
||||
*/
|
||||
'usageRaw': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UsageByUserDto
|
||||
*/
|
||||
'usage': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -2248,6 +2366,46 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
* @summary
|
||||
* @param {CheckExistingAssetsDto} checkExistingAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
checkExistingAssets: async (checkExistingAssetsDto: CheckExistingAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'checkExistingAssetsDto' is not null or undefined
|
||||
assertParamExists('checkExistingAssets', 'checkExistingAssetsDto', checkExistingAssetsDto)
|
||||
const localVarPath = `/asset/exist`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(checkExistingAssetsDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {DeleteAssetDto} deleteAssetDto
|
||||
@@ -2867,6 +3025,17 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.checkDuplicateAsset(checkDuplicateAssetDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
* @summary
|
||||
* @param {CheckExistingAssetsDto} checkExistingAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CheckExistingAssetsResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.checkExistingAssets(checkExistingAssetsDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {DeleteAssetDto} deleteAssetDto
|
||||
@@ -3042,6 +3211,16 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
checkDuplicateAsset(checkDuplicateAssetDto: CheckDuplicateAssetDto, options?: any): AxiosPromise<CheckDuplicateAssetResponseDto> {
|
||||
return localVarFp.checkDuplicateAsset(checkDuplicateAssetDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
* @summary
|
||||
* @param {CheckExistingAssetsDto} checkExistingAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: any): AxiosPromise<CheckExistingAssetsResponseDto> {
|
||||
return localVarFp.checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {DeleteAssetDto} deleteAssetDto
|
||||
@@ -3204,6 +3383,18 @@ export class AssetApi extends BaseAPI {
|
||||
return AssetApiFp(this.configuration).checkDuplicateAsset(checkDuplicateAssetDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
* @summary
|
||||
* @param {CheckExistingAssetsDto} checkExistingAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DeleteAssetDto} deleteAssetDto
|
||||
@@ -4132,6 +4323,35 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getStats: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/server-info/stats`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
@@ -4198,6 +4418,15 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getServerVersion(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getStats(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerStatsResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getStats(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
@@ -4233,6 +4462,14 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
|
||||
getServerVersion(options?: any): AxiosPromise<ServerVersionReponseDto> {
|
||||
return localVarFp.getServerVersion(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getStats(options?: any): AxiosPromise<ServerStatsResponseDto> {
|
||||
return localVarFp.getStats(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
@@ -4271,6 +4508,16 @@ export class ServerInfoApi extends BaseAPI {
|
||||
return ServerInfoApiFp(this.configuration).getServerVersion(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ServerInfoApi
|
||||
*/
|
||||
public getStats(options?: AxiosRequestConfig) {
|
||||
return ServerInfoApiFp(this.configuration).getStats(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
|
||||
@@ -48,7 +48,7 @@ html::-webkit-scrollbar-thumb:hover {
|
||||
body {
|
||||
/* min-height: 100vh; */
|
||||
margin: 0;
|
||||
background-color: #f6f8fe;
|
||||
/* background-color: #f6f8fe; */
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
@@ -59,15 +59,15 @@ input:focus-visible {
|
||||
|
||||
@layer utilities {
|
||||
.immich-form-input {
|
||||
@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm;
|
||||
@apply bg-slate-100 p-2 rounded-md dark:text-immich-dark-bg focus:border-immich-primary text-sm;
|
||||
}
|
||||
|
||||
.immich-form-label {
|
||||
@apply font-medium text-sm text-gray-500;
|
||||
@apply font-medium text-sm text-gray-500 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.immich-btn-primary {
|
||||
@apply bg-immich-primary text-gray-100 border rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary hover:shadow-lg text-sm font-medium;
|
||||
@apply bg-immich-primary dark:bg-immich-dark-primary dark:text-immich-dark-gray text-gray-100 border dark:border-immich-dark-gray rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary dark:hover:bg-immich-dark-primary/90 hover:shadow-lg text-sm font-medium;
|
||||
}
|
||||
|
||||
.immich-text-button {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
@@ -7,7 +7,7 @@
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,24 +11,28 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<div class="flex border p-6 rounded-2xl bg-white">
|
||||
<div class="flex border-b pb-5 dark:border-b-immich-dark-gray">
|
||||
<div class="w-[70%]">
|
||||
<h1 class="font-medium text-immich-primary">{title}</h1>
|
||||
<p class="text-sm mt-1 font-medium">{subtitle}</p>
|
||||
<p class="text-sm">
|
||||
<h1 class="text-immich-primary dark:text-immich-dark-primary text-sm">{title.toUpperCase()}</h1>
|
||||
<p class="text-sm mt-1 dark:text-immich-dark-fg">{subtitle}</p>
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<slot />
|
||||
</p>
|
||||
<table class="text-left w-full mt-4">
|
||||
<table class="text-left w-full mt-5">
|
||||
<!-- table header -->
|
||||
<thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12">
|
||||
<thead
|
||||
class="border rounded-md mb-2 dark:bg-immich-dark-gray dark:border-immich-dark-gray bg-immich-primary/10 flex text-immich-primary dark:text-immich-dark-primary w-full h-12"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/3 font-medium text-sm">Status</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Active</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Waiting</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border">
|
||||
<tr class="text-center flex place-items-center w-full h-[40px]">
|
||||
<tbody
|
||||
class="overflow-y-auto rounded-md w-full max-h-[320px] block border bg-white dark:border-immich-dark-gray dark:bg-[#e5e5e5] dark:text-immich-dark-bg"
|
||||
>
|
||||
<tr class="text-center flex place-items-center w-full h-[60px]">
|
||||
<td class="text-sm px-2 w-1/3 text-ellipsis">{jobStatus ? 'Active' : 'Idle'}</td>
|
||||
<td class="text-sm px-2 w-1/3 text-ellipsis">{activeJobCount}</td>
|
||||
<td class="text-sm px-2 w-1/3 text-ellipsis">{waitingJobCount}</td>
|
||||
@@ -39,7 +43,7 @@
|
||||
<div class="w-[30%] flex place-items-center place-content-end">
|
||||
<button
|
||||
on:click={() => dispatch('click')}
|
||||
class="border px-6 py-3 text-sm bg-gray-50 font-medium rounded-2xl hover:bg-immich-primary/10 transition-all hover:cursor-pointer disabled:cursor-not-allowed"
|
||||
class="px-6 py-3 text-sm bg-immich-primary dark:bg-immich-dark-primary font-medium rounded-2xl hover:bg-immich-primary/50 transition-all hover:cursor-pointer disabled:cursor-not-allowed shadow-sm text-immich-bg dark:text-immich-dark-gray"
|
||||
disabled={jobStatus}
|
||||
>
|
||||
{#if jobStatus}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-10">
|
||||
<JobTile
|
||||
title={'Generate thumbnails'}
|
||||
subtitle={'Regenerate missing thumbnail (JPEG, WEBP)'}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { ServerStatsResponseDto, UserResponseDto } from '@api';
|
||||
import CameraIris from 'svelte-material-icons/CameraIris.svelte';
|
||||
import PlayCircle from 'svelte-material-icons/PlayCircle.svelte';
|
||||
import FileImageOutline from 'svelte-material-icons/FileImageOutline.svelte';
|
||||
import Memory from 'svelte-material-icons/Memory.svelte';
|
||||
import StatsCard from './stats-card.svelte';
|
||||
export let stats: ServerStatsResponseDto;
|
||||
export let allUsers: Array<UserResponseDto>;
|
||||
|
||||
const getFullName = (userId: string) => {
|
||||
let name = 'Admin'; // since we do not have admin user in allUsers
|
||||
allUsers.forEach((user) => {
|
||||
if (user.id === userId) name = `${user.firstName} ${user.lastName}`;
|
||||
});
|
||||
return name;
|
||||
};
|
||||
|
||||
$: spaceUnit = stats.usage.slice(stats.usage.length - 2, stats.usage.length);
|
||||
$: spaceUsage = stats.usage.slice(0, stats.usage.length - 2);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-5">
|
||||
<div>
|
||||
<p class="text-sm dark:text-immich-dark-fg">TOTAL USAGE</p>
|
||||
|
||||
<div class="flex mt-5 justify-between">
|
||||
<StatsCard logo={CameraIris} title={'PHOTOS'} value={stats.photos.toString()} />
|
||||
<StatsCard logo={PlayCircle} title={'VIDEOS'} value={stats.videos.toString()} />
|
||||
<StatsCard logo={FileImageOutline} title={'OBJECTS'} value={stats.objects.toString()} />
|
||||
<StatsCard logo={Memory} title={'STORAGE'} value={spaceUsage} unit={spaceUnit} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm dark:text-immich-dark-fg">USER USAGE DETAIL</p>
|
||||
<table class="text-left w-full mt-5">
|
||||
<thead
|
||||
class="border rounded-md mb-4 bg-gray-50 dark:bg-immich-dark-gray dark:border-immich-dark-gray flex text-immich-primary dark:text-immich-dark-primary w-full h-12"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/5 font-medium text-sm">User</th>
|
||||
<th class="text-center w-1/5 font-medium text-sm">Photos</th>
|
||||
<th class="text-center w-1/5 font-medium text-sm">Videos</th>
|
||||
<th class="text-center w-1/5 font-medium text-sm">Objects</th>
|
||||
<th class="text-center w-1/5 font-medium text-sm">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray dark:text-immich-dark-bg"
|
||||
>
|
||||
{#each stats.usageByUser as user, i}
|
||||
<tr
|
||||
class={`text-center flex place-items-center w-full h-[50px] ${
|
||||
i % 2 == 0 ? 'bg-immich-gray dark:bg-[#e5e5e5]' : 'bg-immich-bg dark:bg-[#eeeeee]'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{getFullName(user.userId)}</td>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.photos}</td>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.videos}</td>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.objects}</td>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.usage}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export let logo: any;
|
||||
export let title: string;
|
||||
export let value: string;
|
||||
export let unit: string | undefined = undefined;
|
||||
|
||||
$: zeros = () => {
|
||||
let result = '';
|
||||
const maxLength = 9;
|
||||
const valueLength = parseInt(value).toString().length;
|
||||
const zeroLength = maxLength - valueLength;
|
||||
for (let i = 0; i < zeroLength; i++) {
|
||||
result += '0';
|
||||
}
|
||||
return result;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-[180px] h-[140px] bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between"
|
||||
>
|
||||
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||
<svelte:component this={logo} size="40" />
|
||||
<p>{title}</p>
|
||||
</div>
|
||||
|
||||
<div class="relative text-center font-mono font-semibold text-2xl">
|
||||
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros()}</span><span
|
||||
class="text-immich-primary dark:text-immich-dark-primary">{parseInt(value)}</span
|
||||
>
|
||||
{#if unit}
|
||||
<span class="absolute -top-5 right-2 text-base font-light text-gray-400">{unit}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -8,8 +8,10 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<table class="text-left w-full my-4">
|
||||
<thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12 ">
|
||||
<table class="text-left w-full my-5">
|
||||
<thead
|
||||
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/4 font-medium text-sm">Email</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">First name</th>
|
||||
@@ -17,11 +19,13 @@
|
||||
<th class="text-center w-1/4 font-medium text-sm">Edit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border">
|
||||
<tbody
|
||||
class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray"
|
||||
>
|
||||
{#each allUsers as user, i}
|
||||
<tr
|
||||
class={`text-center flex place-items-center w-full border-b h-[80px] ${
|
||||
i % 2 == 0 ? 'bg-gray-100' : 'bg-immich-bg'
|
||||
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-bg ${
|
||||
i % 2 == 0 ? 'bg-immich-gray dark:bg-[#e5e5e5]' : 'bg-immich-bg dark:bg-[#eeeeee]'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
|
||||
@@ -32,7 +36,7 @@
|
||||
on:click={() => {
|
||||
dispatch('edit-user', { user });
|
||||
}}
|
||||
class="bg-immich-primary text-gray-100 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
><PencilOutline size="20" /></button
|
||||
></td
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user