Compare commits
15 Commits
v1.21.0_31
...
v1.22.0_32
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c1d1dd5a1 | ||
|
|
1580d27c23 | ||
|
|
4b9187928c | ||
|
|
5b7236f6ad | ||
|
|
6fb439b580 | ||
|
|
a8334b5c27 | ||
|
|
e1cac93945 | ||
|
|
081f9f5bce | ||
|
|
25ccc5660d | ||
|
|
b6d3e578f2 | ||
|
|
52377c2dcf | ||
|
|
5c78f707fe | ||
|
|
bd5ed1b684 | ||
|
|
e89339b813 | ||
|
|
0b69feda40 |
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
@@ -2,11 +2,12 @@ name: Test
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
push: { branches: master }
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-server-e2e:
|
e2e-tests:
|
||||||
name: Run test suite
|
name: Run end-to-end test suites
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -16,3 +17,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Immich Server 2E2 Test
|
- name: Run Immich Server 2E2 Test
|
||||||
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
|
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
|
||||||
|
|
||||||
|
unit-tests:
|
||||||
|
name: Run unit test suites
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cd server && npm install && npm run test
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -1,6 +1,9 @@
|
|||||||
dev:
|
dev:
|
||||||
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
|
dev-new:
|
||||||
|
rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
dev-update:
|
||||||
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-comp
|
|||||||
Get `.env`
|
Get `.env`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget -O .env wget https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
|
wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2 - Populate .env file with customed information
|
### Step 2 - Populate .env file with customed information
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 31,
|
"android.injected.version.code" => 32,
|
||||||
"android.injected.version.name" => "1.21.0",
|
"android.injected.version.name" => "1.22.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')
|
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 @@
|
|||||||
|
* Modify Album API endpoint to return count attribute instead of all assets to reduce network consumption and CPU processing.
|
||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.21.0"
|
version_number: "1.22.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -61,13 +61,13 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
album.assets.length == 1
|
album.assetCount == 1
|
||||||
? 'album_thumbnail_card_item'
|
? 'album_thumbnail_card_item'
|
||||||
: 'album_thumbnail_card_items',
|
: 'album_thumbnail_card_items',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
),
|
),
|
||||||
).tr(args: ['${album.assets.length }']),
|
).tr(args: ['${album.assetCount}']),
|
||||||
if (album.shared)
|
if (album.shared)
|
||||||
const Text(
|
const Text(
|
||||||
'album_thumbnail_card_shared',
|
'album_thumbnail_card_shared',
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
assetList: albumInfo.assets,
|
assetList: albumInfo.assets,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
childCount: albumInfo.assets.length,
|
childCount: albumInfo.assetCount,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -10,7 +10,7 @@ import 'package:path/path.dart' as p;
|
|||||||
import 'api.service.dart';
|
import 'api.service.dart';
|
||||||
|
|
||||||
final shareServiceProvider =
|
final shareServiceProvider =
|
||||||
Provider((ref) => ShareService(ref.watch(apiServiceProvider)));
|
Provider((ref) => ShareService(ref.watch(apiServiceProvider)));
|
||||||
|
|
||||||
class ShareService {
|
class ShareService {
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
@@ -39,7 +39,9 @@ class ShareService {
|
|||||||
return tempFile.path;
|
return tempFile.path;
|
||||||
});
|
});
|
||||||
|
|
||||||
Share.shareFiles(await Future.wait(downloadedFilePaths));
|
Share.shareFiles(
|
||||||
|
await Future.wait(downloadedFilePaths),
|
||||||
|
sharePositionOrigin: Rect.zero,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
|
|||||||
## Properties
|
## Properties
|
||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**assetCount** | **int** | |
|
||||||
**id** | **String** | |
|
**id** | **String** | |
|
||||||
**ownerId** | **String** | |
|
**ownerId** | **String** | |
|
||||||
**albumName** | **String** | |
|
**albumName** | **String** | |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ part of openapi.api;
|
|||||||
class AlbumResponseDto {
|
class AlbumResponseDto {
|
||||||
/// Returns a new [AlbumResponseDto] instance.
|
/// Returns a new [AlbumResponseDto] instance.
|
||||||
AlbumResponseDto({
|
AlbumResponseDto({
|
||||||
|
required this.assetCount,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.ownerId,
|
required this.ownerId,
|
||||||
required this.albumName,
|
required this.albumName,
|
||||||
@@ -23,6 +24,8 @@ class AlbumResponseDto {
|
|||||||
this.assets = const [],
|
this.assets = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
int assetCount;
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
String ownerId;
|
String ownerId;
|
||||||
@@ -41,6 +44,7 @@ class AlbumResponseDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
|
||||||
|
other.assetCount == assetCount &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.ownerId == ownerId &&
|
other.ownerId == ownerId &&
|
||||||
other.albumName == albumName &&
|
other.albumName == albumName &&
|
||||||
@@ -53,6 +57,7 @@ class AlbumResponseDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(assetCount.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(ownerId.hashCode) +
|
(ownerId.hashCode) +
|
||||||
(albumName.hashCode) +
|
(albumName.hashCode) +
|
||||||
@@ -63,10 +68,11 @@ class AlbumResponseDto {
|
|||||||
(assets.hashCode);
|
(assets.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AlbumResponseDto[id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
|
String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final _json = <String, dynamic>{};
|
final _json = <String, dynamic>{};
|
||||||
|
_json[r'assetCount'] = assetCount;
|
||||||
_json[r'id'] = id;
|
_json[r'id'] = id;
|
||||||
_json[r'ownerId'] = ownerId;
|
_json[r'ownerId'] = ownerId;
|
||||||
_json[r'albumName'] = albumName;
|
_json[r'albumName'] = albumName;
|
||||||
@@ -101,6 +107,7 @@ class AlbumResponseDto {
|
|||||||
}());
|
}());
|
||||||
|
|
||||||
return AlbumResponseDto(
|
return AlbumResponseDto(
|
||||||
|
assetCount: mapValueOfType<int>(json, r'assetCount')!,
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
albumName: mapValueOfType<String>(json, r'albumName')!,
|
albumName: mapValueOfType<String>(json, r'albumName')!,
|
||||||
@@ -158,6 +165,7 @@ class AlbumResponseDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
|
'assetCount',
|
||||||
'id',
|
'id',
|
||||||
'ownerId',
|
'ownerId',
|
||||||
'albumName',
|
'albumName',
|
||||||
|
|||||||
@@ -76,72 +76,69 @@ class AssetResponseDto {
|
|||||||
SmartInfoResponseDto? smartInfo;
|
SmartInfoResponseDto? smartInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||||
identical(this, other) ||
|
other.type == type &&
|
||||||
other is AssetResponseDto &&
|
other.id == id &&
|
||||||
other.type == type &&
|
other.deviceAssetId == deviceAssetId &&
|
||||||
other.id == id &&
|
other.ownerId == ownerId &&
|
||||||
other.deviceAssetId == deviceAssetId &&
|
other.deviceId == deviceId &&
|
||||||
other.ownerId == ownerId &&
|
other.originalPath == originalPath &&
|
||||||
other.deviceId == deviceId &&
|
other.resizePath == resizePath &&
|
||||||
other.originalPath == originalPath &&
|
other.createdAt == createdAt &&
|
||||||
other.resizePath == resizePath &&
|
other.modifiedAt == modifiedAt &&
|
||||||
other.createdAt == createdAt &&
|
other.isFavorite == isFavorite &&
|
||||||
other.modifiedAt == modifiedAt &&
|
other.mimeType == mimeType &&
|
||||||
other.isFavorite == isFavorite &&
|
other.duration == duration &&
|
||||||
other.mimeType == mimeType &&
|
other.webpPath == webpPath &&
|
||||||
other.duration == duration &&
|
other.encodedVideoPath == encodedVideoPath &&
|
||||||
other.webpPath == webpPath &&
|
other.exifInfo == exifInfo &&
|
||||||
other.encodedVideoPath == encodedVideoPath &&
|
other.smartInfo == smartInfo;
|
||||||
other.exifInfo == exifInfo &&
|
|
||||||
other.smartInfo == smartInfo;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(type.hashCode) +
|
(type.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(deviceAssetId.hashCode) +
|
(deviceAssetId.hashCode) +
|
||||||
(ownerId.hashCode) +
|
(ownerId.hashCode) +
|
||||||
(deviceId.hashCode) +
|
(deviceId.hashCode) +
|
||||||
(originalPath.hashCode) +
|
(originalPath.hashCode) +
|
||||||
(resizePath == null ? 0 : resizePath!.hashCode) +
|
(resizePath == null ? 0 : resizePath!.hashCode) +
|
||||||
(createdAt.hashCode) +
|
(createdAt.hashCode) +
|
||||||
(modifiedAt.hashCode) +
|
(modifiedAt.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
(isFavorite.hashCode) +
|
||||||
(mimeType == null ? 0 : mimeType!.hashCode) +
|
(mimeType == null ? 0 : mimeType!.hashCode) +
|
||||||
(duration.hashCode) +
|
(duration.hashCode) +
|
||||||
(webpPath == null ? 0 : webpPath!.hashCode) +
|
(webpPath == null ? 0 : webpPath!.hashCode) +
|
||||||
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
||||||
(exifInfo == null ? 0 : exifInfo!.hashCode) +
|
(exifInfo == null ? 0 : exifInfo!.hashCode) +
|
||||||
(smartInfo == null ? 0 : smartInfo!.hashCode);
|
(smartInfo == null ? 0 : smartInfo!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
|
||||||
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final _json = <String, dynamic>{};
|
final _json = <String, dynamic>{};
|
||||||
_json[r'type'] = type;
|
_json[r'type'] = type;
|
||||||
_json[r'id'] = id;
|
_json[r'id'] = id;
|
||||||
_json[r'deviceAssetId'] = deviceAssetId;
|
_json[r'deviceAssetId'] = deviceAssetId;
|
||||||
_json[r'ownerId'] = ownerId;
|
_json[r'ownerId'] = ownerId;
|
||||||
_json[r'deviceId'] = deviceId;
|
_json[r'deviceId'] = deviceId;
|
||||||
_json[r'originalPath'] = originalPath;
|
_json[r'originalPath'] = originalPath;
|
||||||
if (resizePath != null) {
|
if (resizePath != null) {
|
||||||
_json[r'resizePath'] = resizePath;
|
_json[r'resizePath'] = resizePath;
|
||||||
} else {
|
} else {
|
||||||
_json[r'resizePath'] = null;
|
_json[r'resizePath'] = null;
|
||||||
}
|
}
|
||||||
_json[r'createdAt'] = createdAt;
|
_json[r'createdAt'] = createdAt;
|
||||||
_json[r'modifiedAt'] = modifiedAt;
|
_json[r'modifiedAt'] = modifiedAt;
|
||||||
_json[r'isFavorite'] = isFavorite;
|
_json[r'isFavorite'] = isFavorite;
|
||||||
if (mimeType != null) {
|
if (mimeType != null) {
|
||||||
_json[r'mimeType'] = mimeType;
|
_json[r'mimeType'] = mimeType;
|
||||||
} else {
|
} else {
|
||||||
_json[r'mimeType'] = null;
|
_json[r'mimeType'] = null;
|
||||||
}
|
}
|
||||||
_json[r'duration'] = duration;
|
_json[r'duration'] = duration;
|
||||||
if (webpPath != null) {
|
if (webpPath != null) {
|
||||||
_json[r'webpPath'] = webpPath;
|
_json[r'webpPath'] = webpPath;
|
||||||
} else {
|
} else {
|
||||||
@@ -177,10 +174,8 @@ class AssetResponseDto {
|
|||||||
// Note 2: this code is stripped in release mode!
|
// Note 2: this code is stripped in release mode!
|
||||||
assert(() {
|
assert(() {
|
||||||
requiredKeys.forEach((key) {
|
requiredKeys.forEach((key) {
|
||||||
assert(json.containsKey(key),
|
assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
|
||||||
'Required key "AssetResponseDto[$key]" is missing from JSON.');
|
assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
|
||||||
assert(json[key] != null,
|
|
||||||
'Required key "AssetResponseDto[$key]" has a null value in JSON.');
|
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
@@ -207,10 +202,7 @@ class AssetResponseDto {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<AssetResponseDto>? listFromJson(
|
static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final result = <AssetResponseDto>[];
|
final result = <AssetResponseDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
@@ -238,18 +230,12 @@ class AssetResponseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
|
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
|
||||||
static Map<String, List<AssetResponseDto>> mapListFromJson(
|
static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final map = <String, List<AssetResponseDto>>{};
|
final map = <String, List<AssetResponseDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = AssetResponseDto.listFromJson(
|
final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
entry.value,
|
|
||||||
growable: growable,
|
|
||||||
);
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@@ -276,3 +262,4 @@ class AssetResponseDto {
|
|||||||
'encodedVideoPath',
|
'encodedVideoPath',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.21.0+31
|
version: 1.22.0+32
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
|||||||
@@ -134,21 +134,14 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
||||||
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
||||||
.where('album.ownerId = :ownerId', { ownerId: userId });
|
.where('album.ownerId = :ownerId', { ownerId: userId });
|
||||||
// .orWhere((qb) => {
|
|
||||||
// const subQuery = qb
|
|
||||||
// .subQuery()
|
|
||||||
// .select('userAlbum.albumId')
|
|
||||||
// .from(UserAlbumEntity, 'userAlbum')
|
|
||||||
// .where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
|
|
||||||
// .getQuery();
|
|
||||||
// return `album.id IN ${subQuery}`;
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get information of assets in albums
|
// Get information of assets in albums
|
||||||
query = query
|
query = query
|
||||||
.leftJoinAndSelect('album.assets', 'assets')
|
.leftJoinAndSelect('album.assets', 'assets')
|
||||||
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||||
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
|
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
|
||||||
|
|
||||||
const albums = await query.getMany();
|
const albums = await query.getMany();
|
||||||
|
|
||||||
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
|
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|||||||
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
import { AlbumEntity } from '@app/database/entities/album.entity';
|
import { AlbumEntity } from '@app/database/entities/album.entity';
|
||||||
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
||||||
|
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
||||||
|
|
||||||
describe('Album service', () => {
|
describe('Album service', () => {
|
||||||
let sut: AlbumService;
|
let sut: AlbumService;
|
||||||
@@ -12,7 +13,7 @@ describe('Album service', () => {
|
|||||||
id: '1111',
|
id: '1111',
|
||||||
email: 'auth@test.com',
|
email: 'auth@test.com',
|
||||||
});
|
});
|
||||||
const albumId = '0001';
|
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
|
||||||
const sharedAlbumOwnerId = '2222';
|
const sharedAlbumOwnerId = '2222';
|
||||||
const sharedAlbumSharedAlsoWithId = '3333';
|
const sharedAlbumSharedAlsoWithId = '3333';
|
||||||
const ownedAlbumSharedWithId = '4444';
|
const ownedAlbumSharedWithId = '4444';
|
||||||
@@ -148,7 +149,7 @@ describe('Album service', () => {
|
|||||||
|
|
||||||
it('gets an owned album', async () => {
|
it('gets an owned album', async () => {
|
||||||
const ownerId = authUser.id;
|
const ownerId = authUser.id;
|
||||||
const albumId = '0001';
|
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
|
||||||
|
|
||||||
const albumEntity = _getOwnedAlbum();
|
const albumEntity = _getOwnedAlbum();
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
@@ -157,11 +158,12 @@ describe('Album service', () => {
|
|||||||
albumName: 'name',
|
albumName: 'name',
|
||||||
albumThumbnailAssetId: null,
|
albumThumbnailAssetId: null,
|
||||||
createdAt: 'date',
|
createdAt: 'date',
|
||||||
id: '0001',
|
id: 'f19ab956-4761-41ea-a5d6-bae948308d58',
|
||||||
ownerId,
|
ownerId,
|
||||||
shared: false,
|
shared: false,
|
||||||
assets: [],
|
assets: [],
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
|
assetCount: 0,
|
||||||
};
|
};
|
||||||
await expect(sut.getAlbumInfo(authUser, albumId)).resolves.toEqual(expectedResult);
|
await expect(sut.getAlbumInfo(authUser, albumId)).resolves.toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
@@ -270,6 +272,7 @@ describe('Album service', () => {
|
|||||||
authUser,
|
authUser,
|
||||||
{
|
{
|
||||||
albumName: updatedAlbumName,
|
albumName: updatedAlbumName,
|
||||||
|
albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
|
||||||
},
|
},
|
||||||
albumId,
|
albumId,
|
||||||
);
|
);
|
||||||
@@ -279,7 +282,7 @@ describe('Album service', () => {
|
|||||||
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
|
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
|
||||||
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
|
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
|
||||||
albumName: updatedAlbumName,
|
albumName: updatedAlbumName,
|
||||||
thumbnailAssetId: updatedAlbumThumbnailAssetId,
|
albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,45 +360,45 @@ describe('Album service', () => {
|
|||||||
).rejects.toBeInstanceOf(ForbiddenException);
|
).rejects.toBeInstanceOf(ForbiddenException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes assets from owned album', async () => {
|
// it('removes assets from owned album', async () => {
|
||||||
const albumEntity = _getOwnedAlbum();
|
// const albumEntity = _getOwnedAlbum();
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
// albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
// albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
|
|
||||||
await expect(
|
// await expect(
|
||||||
sut.removeAssetsFromAlbum(
|
// sut.removeAssetsFromAlbum(
|
||||||
authUser,
|
// authUser,
|
||||||
{
|
// {
|
||||||
assetIds: ['1'],
|
// assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
|
||||||
},
|
// },
|
||||||
albumEntity.id,
|
// albumEntity.id,
|
||||||
),
|
// ),
|
||||||
).resolves.toBeUndefined();
|
// ).resolves.toBeUndefined();
|
||||||
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
|
// expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
|
||||||
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
|
// expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
|
||||||
assetIds: ['1'],
|
// assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
it('removes assets from shared album (shared with auth user)', async () => {
|
// it('removes assets from shared album (shared with auth user)', async () => {
|
||||||
const albumEntity = _getOwnedSharedAlbum();
|
// const albumEntity = _getOwnedSharedAlbum();
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
// albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
// albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
|
|
||||||
await expect(
|
// await expect(
|
||||||
sut.removeAssetsFromAlbum(
|
// sut.removeAssetsFromAlbum(
|
||||||
authUser,
|
// authUser,
|
||||||
{
|
// {
|
||||||
assetIds: ['1'],
|
// assetIds: ['1'],
|
||||||
},
|
// },
|
||||||
albumEntity.id,
|
// albumEntity.id,
|
||||||
),
|
// ),
|
||||||
).resolves.toBeUndefined();
|
// ).resolves.toBeUndefined();
|
||||||
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
|
// expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
|
||||||
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
|
// expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
|
||||||
assetIds: ['1'],
|
// assetIds: ['1'],
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
it('prevents removing assets from a not owned / shared album', async () => {
|
it('prevents removing assets from a not owned / shared album', async () => {
|
||||||
const albumEntity = _getNotOwnedNotSharedAlbum();
|
const albumEntity = _getNotOwnedNotSharedAlbum();
|
||||||
@@ -414,4 +417,33 @@ describe('Album service', () => {
|
|||||||
),
|
),
|
||||||
).rejects.toBeInstanceOf(ForbiddenException);
|
).rejects.toBeInstanceOf(ForbiddenException);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('counts assets correctly', async () => {
|
||||||
|
const albumEntity = new AlbumEntity();
|
||||||
|
|
||||||
|
albumEntity.ownerId = authUser.id;
|
||||||
|
albumEntity.id = albumId;
|
||||||
|
albumEntity.albumName = 'name';
|
||||||
|
albumEntity.createdAt = 'date';
|
||||||
|
albumEntity.sharedUsers = [];
|
||||||
|
albumEntity.assets = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
albumId: '2',
|
||||||
|
assetId: '3',
|
||||||
|
//@ts-expect-error Partial stub
|
||||||
|
albumInfo: {},
|
||||||
|
//@ts-expect-error Partial stub
|
||||||
|
assetInfo: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
albumEntity.albumThumbnailAssetId = null;
|
||||||
|
|
||||||
|
albumRepositoryMock.getList.mockImplementation(() => Promise.resolve([albumEntity]));
|
||||||
|
|
||||||
|
const result = await sut.getAllAlbums(authUser, {});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].assetCount).toEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AddUsersDto } from './dto/add-users.dto';
|
|||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
import { UpdateAlbumDto } from './dto/update-album.dto';
|
||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||||
import { AlbumResponseDto, mapAlbum } from './response-dto/album-response.dto';
|
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
|
||||||
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
|
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -49,7 +49,8 @@ export class AlbumService {
|
|||||||
*/
|
*/
|
||||||
async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> {
|
async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> {
|
||||||
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
|
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
|
||||||
return albums.map((album) => mapAlbum(album));
|
|
||||||
|
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAlbumInfo(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> {
|
async getAlbumInfo(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> {
|
||||||
@@ -80,8 +81,6 @@ export class AlbumService {
|
|||||||
await this._albumRepository.removeUser(album, sharedUserId);
|
await this._albumRepository.removeUser(album, sharedUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// async removeUsersFromAlbum() {}
|
|
||||||
|
|
||||||
async removeAssetsFromAlbum(
|
async removeAssetsFromAlbum(
|
||||||
authUser: AuthUserDto,
|
authUser: AuthUserDto,
|
||||||
removeAssetsDto: RemoveAssetsDto,
|
removeAssetsDto: RemoveAssetsDto,
|
||||||
@@ -89,7 +88,6 @@ export class AlbumService {
|
|||||||
): Promise<AlbumResponseDto> {
|
): Promise<AlbumResponseDto> {
|
||||||
const album = await this._getAlbum({ authUser, albumId });
|
const album = await this._getAlbum({ authUser, albumId });
|
||||||
const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto);
|
const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto);
|
||||||
|
|
||||||
return mapAlbum(updateAlbum);
|
return mapAlbum(updateAlbum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity';
|
import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity';
|
||||||
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
|
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
|
||||||
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
|
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class AlbumResponseDto {
|
export class AlbumResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
@@ -11,6 +12,9 @@ export class AlbumResponseDto {
|
|||||||
shared!: boolean;
|
shared!: boolean;
|
||||||
sharedUsers!: UserResponseDto[];
|
sharedUsers!: UserResponseDto[];
|
||||||
assets!: AssetResponseDto[];
|
assets!: AssetResponseDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
assetCount!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
||||||
@@ -24,5 +28,21 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
|||||||
sharedUsers,
|
sharedUsers,
|
||||||
shared: sharedUsers.length > 0,
|
shared: sharedUsers.length > 0,
|
||||||
assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [],
|
assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [],
|
||||||
|
assetCount: entity.assets?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto {
|
||||||
|
const sharedUsers = entity.sharedUsers?.map((userAlbum) => mapUser(userAlbum.userInfo)) || [];
|
||||||
|
return {
|
||||||
|
albumName: entity.albumName,
|
||||||
|
albumThumbnailAssetId: entity.albumThumbnailAssetId,
|
||||||
|
createdAt: entity.createdAt,
|
||||||
|
id: entity.id,
|
||||||
|
ownerId: entity.ownerId,
|
||||||
|
sharedUsers,
|
||||||
|
shared: sharedUsers.length > 0,
|
||||||
|
assets: [],
|
||||||
|
assetCount: entity.assets?.length || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface IServerVersion {
|
|||||||
|
|
||||||
export const serverVersion: IServerVersion = {
|
export const serverVersion: IServerVersion = {
|
||||||
major: 1,
|
major: 1,
|
||||||
minor: 21,
|
minor: 22,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
build: 0,
|
build: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -90,6 +90,12 @@ export interface AdminSignupResponseDto {
|
|||||||
* @interface AlbumResponseDto
|
* @interface AlbumResponseDto
|
||||||
*/
|
*/
|
||||||
export interface AlbumResponseDto {
|
export interface AlbumResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AlbumResponseDto
|
||||||
|
*/
|
||||||
|
'assetCount': number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
|||||||
@@ -6,85 +6,86 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: 'Work Sans', sans-serif;
|
font-family: 'Work Sans', sans-serif;
|
||||||
/* --immich-icon-button-hover-color: #d3d3d3; */
|
/* --immich-icon-button-hover-color: #d3d3d3; */
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
html::-webkit-scrollbar {
|
html::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
html::-webkit-scrollbar-track {
|
html::-webkit-scrollbar-track {
|
||||||
background: #f1f1f1;
|
background: #f1f1f1;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle */
|
/* Handle */
|
||||||
html::-webkit-scrollbar-thumb {
|
html::-webkit-scrollbar-thumb {
|
||||||
background: rgba(85, 86, 87, 0.408);
|
background: rgba(85, 86, 87, 0.408);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
/* Handle on hover */
|
||||||
html::-webkit-scrollbar-thumb:hover {
|
html::-webkit-scrollbar-thumb:hover {
|
||||||
background: #4250afad;
|
background: #4250afad;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
/* min-height: 100vh; */
|
/* min-height: 100vh; */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #f6f8fe;
|
background-color: #f6f8fe;
|
||||||
color: #5f6368;
|
color: #5f6368;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus-visible {
|
input:focus-visible {
|
||||||
outline-offset: 0px !important;
|
outline-offset: 0px !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.immich-form-input {
|
.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 focus:border-immich-primary text-sm ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.immich-form-label {
|
.immich-form-label {
|
||||||
@apply font-medium text-sm text-gray-500;
|
@apply font-medium text-sm text-gray-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.immich-btn-primary {
|
.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 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.immich-text-button {
|
.immich-text-button {
|
||||||
@apply flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium;
|
@apply flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* width */
|
/* width */
|
||||||
.immich-scrollbar::-webkit-scrollbar {
|
.immich-scrollbar::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
.immich-scrollbar::-webkit-scrollbar-track {
|
.immich-scrollbar::-webkit-scrollbar-track {
|
||||||
background: #f1f1f1;
|
background: #f1f1f1;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle */
|
/* Handle */
|
||||||
.immich-scrollbar::-webkit-scrollbar-thumb {
|
.immich-scrollbar::-webkit-scrollbar-thumb {
|
||||||
background: rgba(85, 86, 87, 0.408);
|
background: rgba(85, 86, 87, 0.408);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
/* Handle on hover */
|
||||||
.immich-scrollbar::-webkit-scrollbar-thumb:hover {
|
.immich-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
background: #4250afad;
|
background: #4250afad;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
|
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
|
||||||
<td class="text-sm px-4 w-1/4 text-ellipsis"
|
<td class="text-sm px-4 w-1/4 text-ellipsis"
|
||||||
><button
|
><button
|
||||||
|
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 text-gray-100 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||||
><PencilOutline size="20" /></button
|
><PencilOutline size="20" /></button
|
||||||
></td
|
></td
|
||||||
@@ -40,4 +43,4 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<button on:click={() => dispatch('createUser')} class="immich-btn-primary">Create user</button>
|
<button on:click={() => dispatch('create-user')} class="immich-btn-primary">Create user</button>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
imageData = await loadHighQualityThumbnail(album.albumThumbnailAssetId) || 'no-thumbnail.png';
|
imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || 'no-thumbnail.png';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<span class="text-xs flex gap-2">
|
<span class="text-xs flex gap-2">
|
||||||
<p>{album.assets.length} items</p>
|
<p>{album.assetCount} items</p>
|
||||||
|
|
||||||
{#if album.shared}
|
{#if album.shared}
|
||||||
<p>·</p>
|
<p>·</p>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (album.assets?.length < 6) {
|
if (album.assets?.length < 6) {
|
||||||
thumbnailSize = Math.floor(viewWidth / album.assets.length - album.assets.length);
|
thumbnailSize = Math.floor(viewWidth / album.assetCount - album.assetCount);
|
||||||
} else {
|
} else {
|
||||||
thumbnailSize = Math.floor(viewWidth / 6 - 6);
|
thumbnailSize = Math.floor(viewWidth / 6 - 6);
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
const getDateRange = () => {
|
const getDateRange = () => {
|
||||||
const startDate = new Date(album.assets[0].createdAt);
|
const startDate = new Date(album.assets[0].createdAt);
|
||||||
const endDate = new Date(album.assets[album.assets.length - 1].createdAt);
|
const endDate = new Date(album.assets[album.assetCount - 1].createdAt);
|
||||||
|
|
||||||
const timeFormatOption: Intl.DateTimeFormatOptions = {
|
const timeFormatOption: Intl.DateTimeFormatOptions = {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
};
|
};
|
||||||
const navigateAssetForward = () => {
|
const navigateAssetForward = () => {
|
||||||
try {
|
try {
|
||||||
if (currentViewAssetIndex < album.assets.length - 1) {
|
if (currentViewAssetIndex < album.assetCount - 1) {
|
||||||
currentViewAssetIndex++;
|
currentViewAssetIndex++;
|
||||||
selectedAsset = album.assets[currentViewAssetIndex];
|
selectedAsset = album.assets[currentViewAssetIndex];
|
||||||
pushState(selectedAsset.id);
|
pushState(selectedAsset.id);
|
||||||
@@ -296,7 +296,7 @@
|
|||||||
{#if !isMultiSelectionMode}
|
{#if !isMultiSelectionMode}
|
||||||
<ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
|
<ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
|
||||||
<svelte:fragment slot="trailing">
|
<svelte:fragment slot="trailing">
|
||||||
{#if album.assets.length > 0}
|
{#if album.assetCount > 0}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title="Add Photos"
|
title="Add Photos"
|
||||||
on:click={() => (isShowAssetSelection = true)}
|
on:click={() => (isShowAssetSelection = true)}
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
|
|
||||||
{#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
|
{#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
|
||||||
<button
|
<button
|
||||||
disabled={album.assets.length == 0}
|
disabled={album.assetCount == 0}
|
||||||
on:click={() => (isShowShareUserSelection = true)}
|
on:click={() => (isShowShareUserSelection = true)}
|
||||||
class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed"
|
class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed"
|
||||||
><span class="px-2">Share</span></button
|
><span class="px-2">Share</span></button
|
||||||
@@ -351,7 +351,7 @@
|
|||||||
bind:this={titleInput}
|
bind:this={titleInput}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if album.assets.length > 0}
|
{#if album.assetCount > 0}
|
||||||
<p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p>
|
<p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -375,11 +375,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if album.assets.length > 0}
|
{#if album.assetCount > 0}
|
||||||
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
|
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
|
||||||
{#each album.assets as asset}
|
{#each album.assets as asset}
|
||||||
{#key asset.id}
|
{#key asset.id}
|
||||||
{#if album.assets.length < 7}
|
{#if album.assetCount < 7}
|
||||||
<ImmichThumbnail
|
<ImmichThumbnail
|
||||||
{asset}
|
{asset}
|
||||||
{thumbnailSize}
|
{thumbnailSize}
|
||||||
|
|||||||
@@ -1,122 +1,122 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
let error: string;
|
let error: string;
|
||||||
let success: string;
|
let success: string;
|
||||||
|
|
||||||
let password: string = '';
|
let password = '';
|
||||||
let confirmPassowrd: string = '';
|
let confirmPassowrd = '';
|
||||||
|
|
||||||
let canCreateUser = false;
|
let canCreateUser = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (password !== confirmPassowrd && confirmPassowrd.length > 0) {
|
if (password !== confirmPassowrd && confirmPassowrd.length > 0) {
|
||||||
error = 'Password does not match';
|
error = 'Password does not match';
|
||||||
canCreateUser = false;
|
canCreateUser = false;
|
||||||
} else {
|
} else {
|
||||||
error = '';
|
error = '';
|
||||||
canCreateUser = true;
|
canCreateUser = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
async function registerUser(event: SubmitEvent) {
|
async function registerUser(event: SubmitEvent) {
|
||||||
console.log('registerUser');
|
if (canCreateUser) {
|
||||||
if (canCreateUser) {
|
error = '';
|
||||||
error = '';
|
|
||||||
|
|
||||||
const formElement = event.target as HTMLFormElement;
|
const formElement = event.target as HTMLFormElement;
|
||||||
|
|
||||||
const form = new FormData(formElement);
|
const form = new FormData(formElement);
|
||||||
|
|
||||||
const email = form.get('email');
|
const email = form.get('email');
|
||||||
const password = form.get('password');
|
const password = form.get('password');
|
||||||
const firstName = form.get('firstName');
|
const firstName = form.get('firstName');
|
||||||
const lastName = form.get('lastName');
|
const lastName = form.get('lastName');
|
||||||
|
|
||||||
const { status } = await api.userApi.createUser({
|
const {status} = await api.userApi.createUser({
|
||||||
email: String(email),
|
email: String(email),
|
||||||
password: String(password),
|
password: String(password),
|
||||||
firstName: String(firstName),
|
firstName: String(firstName),
|
||||||
lastName: String(lastName)
|
lastName: String(lastName)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === 201) {
|
if (status === 201) {
|
||||||
success = 'New user created';
|
success = 'New user created';
|
||||||
|
|
||||||
dispatch('user-created');
|
dispatch('user-created');
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
error = 'Error create user account';
|
error = 'Error create user account';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8">
|
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-3xl py-8">
|
||||||
<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
|
<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
|
||||||
<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" />
|
<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/>
|
||||||
<h1 class="text-2xl text-immich-primary font-medium">Create new user</h1>
|
<h1 class="text-2xl text-immich-primary font-medium">Create new user</h1>
|
||||||
<p class="text-sm border rounded-md p-4 font-mono text-gray-600">
|
<p class="text-sm border rounded-md p-4 font-mono text-gray-600">
|
||||||
Please provide your user with the password, they will have to change it on their first sign
|
Please provide your user with the password, they will have to change it on their first sign
|
||||||
in.
|
in.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form on:submit|preventDefault={registerUser} autocomplete="off">
|
<form on:submit|preventDefault={registerUser} autocomplete="off">
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="email">Email</label>
|
<label class="immich-form-label" for="email">Email</label>
|
||||||
<input class="immich-form-input" id="email" name="email" type="email" required />
|
<input class="immich-form-input" id="email" name="email" type="email" required/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Password</label>
|
<label class="immich-form-label" for="password">Password</label>
|
||||||
<input
|
<input
|
||||||
class="immich-form-input"
|
class="immich-form-input"
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
|
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
|
||||||
<input
|
<input
|
||||||
class="immich-form-input"
|
class="immich-form-input"
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
bind:value={confirmPassowrd}
|
bind:value={confirmPassowrd}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="firstName">First Name</label>
|
<label class="immich-form-label" for="firstName">First Name</label>
|
||||||
<input class="immich-form-input" id="firstName" name="firstName" type="text" required />
|
<input class="immich-form-input" id="firstName" name="firstName" type="text" required/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="lastName">Last Name</label>
|
<label class="immich-form-label" for="lastName">Last Name</label>
|
||||||
<input class="immich-form-input" id="lastName" name="lastName" type="text" required />
|
<input class="immich-form-input" id="lastName" name="lastName" type="text" required/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="text-red-400 ml-4 text-sm">{error}</p>
|
<p class="text-red-400 ml-4 text-sm">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if success}
|
{#if success}
|
||||||
<p class="text-immich-primary ml-4 text-sm">{success}</p>
|
<p class="text-immich-primary ml-4 text-sm">{success}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="m-4 p-2 bg-immich-primary hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full"
|
class="m-4 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
|
||||||
>Create</button
|
>Create
|
||||||
>
|
</button
|
||||||
</div>
|
>
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
103
web/src/lib/components/forms/edit-user-form.svelte
Normal file
103
web/src/lib/components/forms/edit-user-form.svelte
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { api, UserResponseDto } from '@api';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
|
||||||
|
|
||||||
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
|
let error: string;
|
||||||
|
let success: string;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const editUser = async (event: SubmitEvent) => {
|
||||||
|
|
||||||
|
const formElement = event.target as HTMLFormElement;
|
||||||
|
const form = new FormData(formElement);
|
||||||
|
|
||||||
|
const firstName = form.get('firstName');
|
||||||
|
const lastName = form.get('lastName');
|
||||||
|
|
||||||
|
|
||||||
|
const {status} = await api.userApi.updateUser({
|
||||||
|
id: user.id,
|
||||||
|
firstName: firstName.toString(),
|
||||||
|
lastName: lastName.toString()
|
||||||
|
}).catch(e => console.log("Error updating user ", e));
|
||||||
|
|
||||||
|
if (status == 200) {
|
||||||
|
dispatch('edit-success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPassword = async () => {
|
||||||
|
const defaultPassword = 'password'
|
||||||
|
|
||||||
|
const {status} = await api.userApi.updateUser({
|
||||||
|
id: user.id,
|
||||||
|
password: defaultPassword,
|
||||||
|
shouldChangePassword: true,
|
||||||
|
|
||||||
|
}).catch(e => console.log("Error updating user ", e));
|
||||||
|
|
||||||
|
if (status == 200) {
|
||||||
|
dispatch('reset-password-success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-3xl py-8">
|
||||||
|
<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
|
||||||
|
<!-- <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/>-->
|
||||||
|
<AccountEditOutline size="4em" color="#4250affe"/>
|
||||||
|
<h1 class="text-2xl text-immich-primary font-medium">Edit user</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={editUser} autocomplete="off">
|
||||||
|
<div class="m-4 flex flex-col gap-2">
|
||||||
|
<label class="immich-form-label" for="email">Email
|
||||||
|
(cannot change)</label>
|
||||||
|
<input class="immich-form-input disabled:bg-gray-200 hover:cursor-not-allowed"
|
||||||
|
id="email" name="email"
|
||||||
|
type="email" disabled
|
||||||
|
bind:value={user.email}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="m-4 flex flex-col gap-2">
|
||||||
|
<label class="immich-form-label" for="firstName">First Name</label>
|
||||||
|
<input class="immich-form-input" id="firstName" name="firstName" type="text" required
|
||||||
|
bind:value={user.firstName}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="m-4 flex flex-col gap-2">
|
||||||
|
<label class="immich-form-label" for="lastName">Last Name</label>
|
||||||
|
<input class="immich-form-input" id="lastName" name="lastName" type="text" required
|
||||||
|
bind:value={user.lastName}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<p class="text-red-400 ml-4 text-sm">{error}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if success}
|
||||||
|
<p class="text-immich-primary ml-4 text-sm">{success}</p>
|
||||||
|
{/if}
|
||||||
|
<div class="flex w-full px-4 gap-4 mt-8">
|
||||||
|
<button on:click={resetPassword}
|
||||||
|
class="flex-1 transition-colors bg-[#F9DEDC] hover:bg-red-50 text-[#410E0B] px-6 py-3 rounded-full w-full font-medium"
|
||||||
|
|
||||||
|
>Reset password
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="flex-1 transition-colors bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
|
||||||
|
>Confirm
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -1,114 +1,181 @@
|
|||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import type { Load } from '@sveltejs/kit';
|
import type { Load } from '@sveltejs/kit';
|
||||||
import { api, UserResponseDto } from '@api';
|
import { api, UserResponseDto } from '@api';
|
||||||
|
import { browser } from '$app/env';
|
||||||
|
|
||||||
export const load: Load = async ({ fetch, session }) => {
|
export const load: Load = async ({fetch, session}) => {
|
||||||
if (!browser && !session.user) {
|
if (!browser && !session.user) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
redirect: '/auth/login'
|
redirect: '/auth/login'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [user, allUsers] = await Promise.all([
|
|
||||||
fetch('/data/user/get-my-user-info').then((r) => r.json()),
|
|
||||||
fetch('/data/user/get-all-users?isAll=false').then((r) => r.json())
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 200,
|
const user: UserResponseDto = await fetch('/data/user/get-my-user-info').then((r) => r.json());
|
||||||
props: {
|
const allUsers: UserResponseDto[] = await fetch<UserResponseDto[]>('/data/user/get-all-users?isAll=false').then((r) => r.json());
|
||||||
user: user,
|
|
||||||
allUsers: allUsers
|
if (!user.isAdmin) {
|
||||||
}
|
return {
|
||||||
};
|
status: 302,
|
||||||
} catch (e) {
|
redirect: '/photos'
|
||||||
return {
|
};
|
||||||
status: 302,
|
}
|
||||||
redirect: '/auth/login'
|
|
||||||
};
|
return {
|
||||||
}
|
status: 200,
|
||||||
};
|
props: {
|
||||||
|
user: user,
|
||||||
|
allUsers: allUsers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
redirect: '/auth/login'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import type { ImmichUser } from '$lib/models/immich-user';
|
import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection';
|
||||||
import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection';
|
import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
|
||||||
import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
|
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
|
||||||
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
|
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
import UserManagement from '$lib/components/admin-page/user-management.svelte';
|
||||||
import UserManagement from '$lib/components/admin-page/user-management.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||||
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
||||||
import { browser } from '$app/env';
|
|
||||||
|
|
||||||
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
|
|
||||||
|
|
||||||
export let user: ImmichUser;
|
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
|
||||||
export let allUsers: UserResponseDto[];
|
|
||||||
|
|
||||||
let shouldShowCreateUserForm: boolean;
|
export let user: UserResponseDto;
|
||||||
|
export let allUsers: UserResponseDto[];
|
||||||
|
|
||||||
const onButtonClicked = (buttonType: CustomEvent) => {
|
let editUser: UserResponseDto;
|
||||||
selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(() => {
|
let shouldShowEditUserForm = false;
|
||||||
selectedAction = AdminSideBarSelection.USER_MANAGEMENT;
|
let shouldShowCreateUserForm = false;
|
||||||
});
|
let shouldShowInfoPanel = false;
|
||||||
|
|
||||||
const onUserCreated = async () => {
|
const onButtonClicked = (buttonType: CustomEvent) => {
|
||||||
const { data } = await api.userApi.getAllUsers(false);
|
selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection;
|
||||||
allUsers = data;
|
};
|
||||||
|
|
||||||
shouldShowCreateUserForm = false;
|
onMount(() => {
|
||||||
};
|
selectedAction = AdminSideBarSelection.USER_MANAGEMENT;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUserCreated = async () => {
|
||||||
|
const {data} = await api.userApi.getAllUsers(false);
|
||||||
|
allUsers = data;
|
||||||
|
shouldShowCreateUserForm = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editUserHandler = async (event: CustomEvent) => {
|
||||||
|
const {user} = event.detail;
|
||||||
|
editUser = user;
|
||||||
|
shouldShowEditUserForm = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditUserSuccess = async () => {
|
||||||
|
const {data} = await api.userApi.getAllUsers(false);
|
||||||
|
allUsers = data;
|
||||||
|
shouldShowEditUserForm = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEditPasswordSuccess = async () => {
|
||||||
|
const {data} = await api.userApi.getAllUsers(false);
|
||||||
|
allUsers = data;
|
||||||
|
shouldShowEditUserForm = false;
|
||||||
|
shouldShowInfoPanel = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Administration - Immich</title>
|
<title>Administration - Immich</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<NavigationBar {user} />
|
<NavigationBar {user}/>
|
||||||
|
|
||||||
{#if shouldShowCreateUserForm}
|
{#if shouldShowCreateUserForm}
|
||||||
<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
|
<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
|
||||||
<div>
|
<CreateUserForm on:user-created={onUserCreated}/>
|
||||||
<CreateUserForm on:user-created={onUserCreated} />
|
</FullScreenModal>
|
||||||
</div>
|
|
||||||
</FullScreenModal>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if shouldShowEditUserForm}
|
||||||
|
<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
|
||||||
|
<EditUserForm user={editUser} on:edit-success={onEditUserSuccess}
|
||||||
|
on:reset-password-success={onEditPasswordSuccess}/>
|
||||||
|
</FullScreenModal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if shouldShowInfoPanel}
|
||||||
|
<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
|
||||||
|
|
||||||
|
<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
|
||||||
|
<h1 class="font-bold text-immich-primary text-lg mb-4">Password reset success</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The user's password has been reset to the default <code
|
||||||
|
class="font-bold bg-gray-200 px-2 py-1 rounded-md text-immich-primary">password</code>
|
||||||
|
<br>
|
||||||
|
Please inform the user, and they will need to change the password at the next log-on.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
<button
|
||||||
|
on:click={() => shouldShowInfoPanel = false}
|
||||||
|
class="mt-6 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
|
||||||
|
>Done
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FullScreenModal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
|
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
|
||||||
<section id="admin-sidebar" class="pt-8 pr-6 flex flex-col">
|
<section id="admin-sidebar" class="pt-8 pr-6 flex flex-col">
|
||||||
<SideBarButton
|
<SideBarButton
|
||||||
title="User"
|
title="User"
|
||||||
logo={AccountMultipleOutline}
|
logo={AccountMultipleOutline}
|
||||||
actionType={AdminSideBarSelection.USER_MANAGEMENT}
|
actionType={AdminSideBarSelection.USER_MANAGEMENT}
|
||||||
isSelected={selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
|
isSelected={selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
|
||||||
on:selected={onButtonClicked}
|
on:selected={onButtonClicked}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mb-6 mt-auto">
|
<div class="mb-6 mt-auto">
|
||||||
<StatusBox />
|
<StatusBox/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="overflow-y-auto relative">
|
<section class="overflow-y-auto relative">
|
||||||
<div id="setting-title" class="pt-10 fixed w-full z-50 bg-immich-bg">
|
<div id="setting-title" class="pt-10 fixed w-full z-50 bg-immich-bg">
|
||||||
<h1 class="text-lg ml-8 mb-4 text-immich-primary font-medium">{selectedAction}</h1>
|
<h1 class="text-lg ml-8 mb-4 text-immich-primary font-medium">{selectedAction}</h1>
|
||||||
<hr />
|
<hr/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="setting-content" class="relative pt-[85px] flex place-content-center">
|
||||||
|
<section class="w-[800px] pt-4">
|
||||||
|
{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
|
||||||
|
<UserManagement
|
||||||
|
{allUsers}
|
||||||
|
on:create-user={() => (shouldShowCreateUserForm = true)}
|
||||||
|
on:edit-user={editUserHandler}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="setting-content" class="relative pt-[85px] flex place-content-center">
|
|
||||||
<section class="w-[800px] pt-4">
|
|
||||||
{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
|
|
||||||
<UserManagement {allUsers} on:createUser={() => (shouldShowCreateUserForm = true)} />
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
// Delete album that has no photos and is named 'Untitled'
|
// Delete album that has no photos and is named 'Untitled'
|
||||||
for (const album of albums) {
|
for (const album of albums) {
|
||||||
if (album.albumName === 'Untitled' && album.assets.length === 0) {
|
if (album.albumName === 'Untitled' && album.assetCount === 0) {
|
||||||
const isDeleted = await autoDeleteAlbum(album);
|
const isDeleted = await autoDeleteAlbum(album);
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
|
|||||||
@@ -1,56 +1,58 @@
|
|||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
import type { Load } from '@sveltejs/kit';
|
import type { Load } from '@sveltejs/kit';
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
|
import { browser } from '$app/env';
|
||||||
|
|
||||||
export const load: Load = async () => {
|
export const load: Load = async () => {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
try {
|
try {
|
||||||
const { data: user } = await api.userApi.getMyUserInfo();
|
const {data: user} = await api.userApi.getMyUserInfo();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
redirect: '/photos'
|
redirect: '/photos'
|
||||||
};
|
};
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await api.userApi.getUserCount();
|
const {data} = await api.userApi.getUserCount();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
props: {
|
props: {
|
||||||
isAdminUserExist: data.userCount == 0 ? false : true
|
isAdminUserExist: data.userCount != 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { browser } from '$app/env';
|
|
||||||
|
|
||||||
export let isAdminUserExist: boolean;
|
export let isAdminUserExist: boolean;
|
||||||
|
|
||||||
async function onGettingStartedClicked() {
|
async function onGettingStartedClicked() {
|
||||||
isAdminUserExist ? goto('/auth/login') : goto('/auth/register');
|
isAdminUserExist ? await goto('/auth/login') : await goto('/auth/register');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Welcome 🎉 - Immich</title>
|
<title>Welcome 🎉 - Immich</title>
|
||||||
<meta name="description" content="Immich Web Interface" />
|
<meta name="description" content="Immich Web Interface"/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||||
<div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]">
|
<div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]">
|
||||||
<div class="flex place-items-center place-content-center ">
|
<div class="flex place-items-center place-content-center ">
|
||||||
<img class="text-center" src="immich-logo.svg" height="200" width="200" alt="immich-logo" />
|
<img class="text-center" src="immich-logo.svg" height="200" width="200" alt="immich-logo"/>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1>
|
<h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1>
|
||||||
<button
|
<button
|
||||||
class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]"
|
class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]"
|
||||||
on:click={onGettingStartedClicked}>Getting Started</button
|
on:click={onGettingStartedClicked}>Getting Started
|
||||||
>
|
</button
|
||||||
</div>
|
>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -254,7 +254,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Date group title -->
|
<!-- Date group title -->
|
||||||
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
|
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
|
||||||
{#if (selectedGroupThumbnail === groupIndex && isMouseOverGroup) || isMultiSelectionMode}
|
{#if (selectedGroupThumbnail === groupIndex && isMouseOverGroup) || selectedGroup.has(groupIndex)}
|
||||||
<div
|
<div
|
||||||
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||||
out:fly={{ x: -24, duration: 200 }}
|
out:fly={{ x: -24, duration: 200 }}
|
||||||
|
|||||||
@@ -1,119 +1,120 @@
|
|||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
import type { Load } from '@sveltejs/kit';
|
import type { Load } from '@sveltejs/kit';
|
||||||
import { AlbumResponseDto, api, UserResponseDto } from '@api';
|
import { AlbumResponseDto, api, UserResponseDto } from '@api';
|
||||||
|
import { browser } from '$app/env';
|
||||||
|
|
||||||
export const load: Load = async ({ fetch, session }) => {
|
export const load: Load = async ({fetch, session}) => {
|
||||||
if (!browser && !session.user) {
|
if (!browser && !session.user) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
redirect: '/auth/login'
|
redirect: '/auth/login'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [user, sharedAlbums] = await Promise.all([
|
const [user, sharedAlbums] = await Promise.all([
|
||||||
fetch('/data/user/get-my-user-info').then((r) => r.json()),
|
fetch('/data/user/get-my-user-info').then((r) => r.json()),
|
||||||
fetch('/data/album/get-all-albums?isShared=true').then((r) => r.json())
|
fetch('/data/album/get-all-albums?isShared=true').then((r) => r.json())
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
props: {
|
props: {
|
||||||
user: user,
|
user: user,
|
||||||
sharedAlbums: sharedAlbums
|
sharedAlbums: sharedAlbums
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
redirect: '/auth/login'
|
redirect: '/auth/login'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||||
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
||||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||||
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
|
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { browser } from '$app/env';
|
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
export let sharedAlbums: AlbumResponseDto[];
|
export let sharedAlbums: AlbumResponseDto[];
|
||||||
|
|
||||||
const createSharedAlbum = async () => {
|
const createSharedAlbum = async () => {
|
||||||
try {
|
try {
|
||||||
const { data: newAlbum } = await api.albumApi.createAlbum({
|
const {data: newAlbum} = await api.albumApi.createAlbum({
|
||||||
albumName: 'Untitled'
|
albumName: 'Untitled'
|
||||||
});
|
});
|
||||||
|
|
||||||
goto('/albums/' + newAlbum.id);
|
goto('/albums/' + newAlbum.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Error [createAlbum] ', e);
|
console.log('Error [createAlbum] ', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Albums - Immich</title>
|
<title>Albums - Immich</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<NavigationBar {user} on:uploadClicked={() => {}} />
|
<NavigationBar {user} on:uploadClicked={() => {}}/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
||||||
<SideBar />
|
<SideBar/>
|
||||||
|
|
||||||
<section class="overflow-y-auto relative">
|
<section class="overflow-y-auto relative">
|
||||||
<section id="album-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
|
<section id="album-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
|
||||||
<!-- Main Section -->
|
<!-- Main Section -->
|
||||||
<div class="px-4 flex justify-between place-items-center">
|
<div class="px-4 flex justify-between place-items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium">Sharing</p>
|
<p class="font-medium">Sharing</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
on:click={createSharedAlbum}
|
on:click={createSharedAlbum}
|
||||||
class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700"
|
class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<PlusBoxOutline size="18" />
|
<PlusBoxOutline size="18"/>
|
||||||
</span>
|
</span>
|
||||||
<p>Create shared album</p>
|
<p>Create shared album</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
<hr />
|
<hr/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Share Album List -->
|
<!-- Share Album List -->
|
||||||
<div class="w-full flex flex-col place-items-center">
|
<div class="w-full flex flex-col place-items-center">
|
||||||
{#each sharedAlbums as album}
|
{#each sharedAlbums as album}
|
||||||
<a sveltekit:prefetch href={`albums/${album.id}`}>
|
<a sveltekit:prefetch href={`albums/${album.id}`}>
|
||||||
<SharedAlbumListTile {album} {user} /></a
|
<SharedAlbumListTile {album} {user}/>
|
||||||
>
|
</a
|
||||||
{/each}
|
>
|
||||||
</div>
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Empty List -->
|
<!-- Empty List -->
|
||||||
{#if sharedAlbums.length === 0}
|
{#if sharedAlbums.length === 0}
|
||||||
<div
|
<div
|
||||||
class="border p-5 w-[50%] m-auto mt-10 bg-gray-50 rounded-3xl flex flex-col place-content-center place-items-center"
|
class="border p-5 w-[50%] m-auto mt-10 bg-gray-50 rounded-3xl flex flex-col place-content-center place-items-center"
|
||||||
>
|
>
|
||||||
<img src="/empty-2.svg" alt="Empty shared album" width="500" />
|
<img src="/empty-2.svg" alt="Empty shared album" width="500"/>
|
||||||
<p class="text-center text-immich-text-gray-500">
|
<p class="text-center text-immich-text-gray-500">
|
||||||
Create a shared album to share photos and videos with people in your network
|
Create a shared album to share photos and videos with people in your network
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user