Compare commits
7 Commits
v1.34.0_53
...
v1.35.0_54
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66640ebfeb | ||
|
|
9057e4b7d0 | ||
|
|
0deb8f4090 | ||
|
|
1633af7af6 | ||
|
|
99da181cfc | ||
|
|
8a9b0347bb | ||
|
|
fe4b307fe6 |
@@ -3,3 +3,17 @@ sidebar_position: 6
|
|||||||
---
|
---
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
|
### What is the difference between the cloud icon on the mobile app?
|
||||||
|
|
||||||
|
| Icon | Description |
|
||||||
|
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
|  | Asset is only available in the cloud and was uploaded from some other device (like the web client) or was deleted from this device after upload |
|
||||||
|
|  | Asset is only available locally and has not yet been backed up |
|
||||||
|
|  | Asset was uploaded from this device and is now backed up in the cloud/server and still available in original on the device |
|
||||||
|
|
||||||
|
### How can I sync an existing directory with Immich's server?
|
||||||
|
Immich doesn't have the mechanism to sync an existing directory with the server. There is however, a helper CLI tool to help you bulk upload the existing photos and videos to the server. You can find the guide to use the CLI tool [here](/docs/usage/bulk-upload.md).
|
||||||
|
|
||||||
|
### Why doesn't Immich watch an existing photo gallery directory?
|
||||||
|
The initial approach of Immich is to become a backup tool, primarily for mobile device usage. Thus, all the assets must be uploaded from the mobile client. The app was architectured to perform that job well.
|
||||||
1
docs/static/img/cloud-done.svg
vendored
Normal file
1
docs/static/img/cloud-done.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#4250af"><path d="M12.65 39q-4 0-6.825-2.825T3 29.3q0-3.7 2.5-6.475Q8 20.05 11.6 19.7q.75-4.65 4.275-7.65 3.525-3 8.225-3 5.2 0 8.825 3.775Q36.55 16.6 36.55 21.9v1.9h.6q3.3-.1 5.575 2.075Q45 28.05 45 31.4q0 3.1-2.25 5.35Q40.5 39 37.4 39Zm7.95-6.2q.3 0 .55-.125.25-.125.5-.325l8.95-9q.3-.3.325-.75.025-.45-.325-.75-.3-.35-.75-.35t-.75.35l-8.45 8.4-4.05-4.05q-.3-.25-.75-.275-.45-.025-.75.275-.35.35-.35.8 0 .45.35.75l4.55 4.6q.2.2.45.325t.5.125Zm-7.95 3.95H37.4q2.2 0 3.775-1.575Q42.75 33.6 42.75 31.4t-1.575-3.75Q39.6 26.1 37.4 26.1h-3.1v-4.2q0-4.4-3.025-7.5-3.025-3.1-7.375-3.1-4.3 0-7.35 3.1t-3.05 7.5h-1q-2.95 0-5.1 2.15-2.15 2.15-2.15 5.3 0 3.1 2.175 5.25t5.225 2.15ZM24 24Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 756 B |
1
docs/static/img/cloud-off.svg
vendored
Normal file
1
docs/static/img/cloud-off.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#4250af"><path d="m42.15 37.1-1.8-1.8q1.15-.8 1.775-1.8t.625-2.4q0-2.1-1.525-3.625T37.55 25.95H34.3V21.9q0-4.3-3.025-7.275-3.025-2.975-7.325-2.975-1.4 0-2.925.425T18.2 13.45l-1.6-1.65q1.8-1.3 3.6-1.85t3.7-.55q5.25 0 8.95 3.7 3.7 3.7 3.7 8.9v1.75h.6q3.3-.05 5.575 2.05Q45 27.9 45 31.1q0 1.55-.675 3.2-.675 1.65-2.175 2.8Zm-1.75 6.55-5-5.05H12.55q-4.05 0-6.8-2.725T3 29.1q0-3.85 2.525-6.4 2.525-2.55 6.075-2.9.05-.75.375-1.875T12.8 16L5.55 8.75q-.3-.3-.325-.775Q5.2 7.5 5.55 7.15q.35-.35.8-.35.45 0 .8.35l34.9 34.9q.3.3.325.775.025.475-.325.825-.35.35-.825.35t-.825-.35Zm-27.85-7.3h20.6L14.6 17.8q-.55.85-.825 1.975Q13.5 20.9 13.5 22h-.95q-3.05 0-5.175 2T5.25 29q0 3.05 2.125 5.2 2.125 2.15 5.175 2.15ZM29.3 24.4ZM23.85 27Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 799 B |
1
docs/static/img/cloud.svg
vendored
Normal file
1
docs/static/img/cloud.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#4250af"><path d="M12.65 39q-4 0-6.825-2.825T3 29.3q0-3.65 2.45-6.45 2.45-2.8 6.15-3.15.75-4.65 4.275-7.65 3.525-3 8.225-3 5.2 0 8.825 3.775Q36.55 16.6 36.55 21.9v1.9h.6q3.3-.1 5.575 2.075Q45 28.05 45 31.4q0 3.1-2.25 5.35Q40.5 39 37.4 39Zm0-2.25H37.4q2.2 0 3.775-1.575Q42.75 33.6 42.75 31.4t-1.575-3.75Q39.6 26.1 37.4 26.1h-3.1v-4.2q0-4.4-3.025-7.5-3.025-3.1-7.375-3.1-4.3 0-7.35 3.1t-3.05 7.5h-.95q-3.05 0-5.175 2.15t-2.125 5.3q0 3.05 2.175 5.225t5.225 2.175ZM24 24Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 545 B |
@@ -134,13 +134,13 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopEngine(result: Result?) {
|
private fun stopEngine(result: Result?) {
|
||||||
|
clearBackgroundNotification()
|
||||||
|
engine?.destroy()
|
||||||
|
engine = null
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
Log.d(TAG, "stopEngine result=${result}")
|
Log.d(TAG, "stopEngine result=${result}")
|
||||||
resolvableFuture.set(result)
|
resolvableFuture.set(result)
|
||||||
}
|
}
|
||||||
engine?.destroy()
|
|
||||||
engine = null
|
|
||||||
clearBackgroundNotification()
|
|
||||||
waitOnSetForegroundAsync()
|
waitOnSetForegroundAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 53,
|
"android.injected.version.code" => 54,
|
||||||
"android.injected.version.name" => "1.34.0",
|
"android.injected.version.name" => "1.35.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 @@
|
|||||||
|
* Local assets are now shown in the app
|
||||||
@@ -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.34.0"
|
version_number: "1.35.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -35,10 +35,12 @@ void main() async {
|
|||||||
await Future.wait([
|
await Future.wait([
|
||||||
Hive.openBox(userInfoBox),
|
Hive.openBox(userInfoBox),
|
||||||
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
|
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
|
||||||
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
|
||||||
Hive.openBox(hiveGithubReleaseInfoBox),
|
Hive.openBox(hiveGithubReleaseInfoBox),
|
||||||
Hive.openBox(userSettingInfoBox),
|
Hive.openBox(userSettingInfoBox),
|
||||||
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
if (!Platform.isAndroid) Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
||||||
|
if (!Platform.isAndroid)
|
||||||
|
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
||||||
|
if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox),
|
||||||
EasyLocalization.ensureInitialized(),
|
EasyLocalization.ensureInitialized(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -86,8 +88,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
|
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
|
ref.read(backupProvider.notifier).resumeBackup();
|
||||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
|
||||||
ref.watch(assetProvider.notifier).getAllAsset();
|
ref.watch(assetProvider.notifier).getAllAsset();
|
||||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class AssetSelectionPageResult {
|
class AssetSelectionPageResult {
|
||||||
final Set<AssetResponseDto> selectedNewAsset;
|
final Set<Asset> selectedNewAsset;
|
||||||
final Set<AssetResponseDto> selectedAdditionalAsset;
|
final Set<Asset> selectedAdditionalAsset;
|
||||||
final bool isAlbumExist;
|
final bool isAlbumExist;
|
||||||
|
|
||||||
AssetSelectionPageResult({
|
AssetSelectionPageResult({
|
||||||
@@ -14,8 +13,8 @@ class AssetSelectionPageResult {
|
|||||||
});
|
});
|
||||||
|
|
||||||
AssetSelectionPageResult copyWith({
|
AssetSelectionPageResult copyWith({
|
||||||
Set<AssetResponseDto>? selectedNewAsset,
|
Set<Asset>? selectedNewAsset,
|
||||||
Set<AssetResponseDto>? selectedAdditionalAsset,
|
Set<Asset>? selectedAdditionalAsset,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
return AssetSelectionPageResult(
|
return AssetSelectionPageResult(
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class AssetSelectionState {
|
class AssetSelectionState {
|
||||||
final Set<String> selectedMonths;
|
final Set<String> selectedMonths;
|
||||||
final Set<AssetResponseDto> selectedNewAssetsForAlbum;
|
final Set<Asset> selectedNewAssetsForAlbum;
|
||||||
final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum;
|
final Set<Asset> selectedAdditionalAssetsForAlbum;
|
||||||
final Set<AssetResponseDto> selectedAssetsInAlbumViewer;
|
final Set<Asset> selectedAssetsInAlbumViewer;
|
||||||
final bool isMultiselectEnable;
|
final bool isMultiselectEnable;
|
||||||
|
|
||||||
/// Indicate the asset selection page is navigated from existing album
|
/// Indicate the asset selection page is navigated from existing album
|
||||||
@@ -22,9 +21,9 @@ class AssetSelectionState {
|
|||||||
|
|
||||||
AssetSelectionState copyWith({
|
AssetSelectionState copyWith({
|
||||||
Set<String>? selectedMonths,
|
Set<String>? selectedMonths,
|
||||||
Set<AssetResponseDto>? selectedNewAssetsForAlbum,
|
Set<Asset>? selectedNewAssetsForAlbum,
|
||||||
Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum,
|
Set<Asset>? selectedAdditionalAssetsForAlbum,
|
||||||
Set<AssetResponseDto>? selectedAssetsInAlbumViewer,
|
Set<Asset>? selectedAssetsInAlbumViewer,
|
||||||
bool? isMultiselectEnable,
|
bool? isMultiselectEnable,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.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.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
@@ -13,7 +14,6 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllAlbums() async {
|
getAllAlbums() async {
|
||||||
|
|
||||||
if (await _albumCacheService.isValid() && state.isEmpty) {
|
if (await _albumCacheService.isValid() && state.isEmpty) {
|
||||||
state = await _albumCacheService.get();
|
state = await _albumCacheService.get();
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
|||||||
|
|
||||||
Future<AlbumResponseDto?> createAlbum(
|
Future<AlbumResponseDto?> createAlbum(
|
||||||
String albumTitle,
|
String albumTitle,
|
||||||
Set<AssetResponseDto> assets,
|
Set<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
AlbumResponseDto? album =
|
AlbumResponseDto? album =
|
||||||
await _albumService.createAlbum(albumTitle, assets, []);
|
await _albumService.createAlbum(albumTitle, assets, []);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
AssetSelectionNotifier()
|
AssetSelectionNotifier()
|
||||||
@@ -22,15 +21,15 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
|
|
||||||
void removeAssetsInMonth(
|
void removeAssetsInMonth(
|
||||||
String removedMonth,
|
String removedMonth,
|
||||||
List<AssetResponseDto> assetsInMonth,
|
List<Asset> assetsInMonth,
|
||||||
) {
|
) {
|
||||||
Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum;
|
Set<Asset> currentAssetList = state.selectedNewAssetsForAlbum;
|
||||||
Set<String> currentMonthList = state.selectedMonths;
|
Set<String> currentMonthList = state.selectedMonths;
|
||||||
|
|
||||||
currentMonthList
|
currentMonthList
|
||||||
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
||||||
|
|
||||||
for (AssetResponseDto asset in assetsInMonth) {
|
for (Asset asset in assetsInMonth) {
|
||||||
currentAssetList.removeWhere((e) => e.id == asset.id);
|
currentAssetList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAdditionalAssets(List<AssetResponseDto> assets) {
|
void addAdditionalAssets(List<Asset> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAdditionalAssetsForAlbum: {
|
selectedAdditionalAssetsForAlbum: {
|
||||||
...state.selectedAdditionalAssetsForAlbum,
|
...state.selectedAdditionalAssetsForAlbum,
|
||||||
@@ -49,7 +48,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) {
|
void addAllAssetsInMonth(String month, List<Asset> assetsInMonth) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedMonths: {...state.selectedMonths, month},
|
selectedMonths: {...state.selectedMonths, month},
|
||||||
selectedNewAssetsForAlbum: {
|
selectedNewAssetsForAlbum: {
|
||||||
@@ -59,7 +58,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addNewAssets(List<AssetResponseDto> assets) {
|
void addNewAssets(List<Asset> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedNewAssetsForAlbum: {
|
selectedNewAssetsForAlbum: {
|
||||||
...state.selectedNewAssetsForAlbum,
|
...state.selectedNewAssetsForAlbum,
|
||||||
@@ -68,20 +67,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedNewAssets(List<AssetResponseDto> assets) {
|
void removeSelectedNewAssets(List<Asset> assets) {
|
||||||
Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum;
|
Set<Asset> currentList = state.selectedNewAssetsForAlbum;
|
||||||
|
|
||||||
for (AssetResponseDto asset in assets) {
|
for (Asset asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) {
|
void removeSelectedAdditionalAssets(List<Asset> assets) {
|
||||||
Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum;
|
Set<Asset> currentList = state.selectedAdditionalAssetsForAlbum;
|
||||||
|
|
||||||
for (AssetResponseDto asset in assets) {
|
for (Asset asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +108,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
void addAssetsInAlbumViewer(List<Asset> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAssetsInAlbumViewer: {
|
selectedAssetsInAlbumViewer: {
|
||||||
...state.selectedAssetsInAlbumViewer,
|
...state.selectedAssetsInAlbumViewer,
|
||||||
@@ -118,10 +117,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
void removeAssetsInAlbumViewer(List<Asset> assets) {
|
||||||
Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer;
|
Set<Asset> currentList = state.selectedAssetsInAlbumViewer;
|
||||||
|
|
||||||
for (AssetResponseDto asset in assets) {
|
for (Asset asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.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.service.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]);
|
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService)
|
||||||
|
: super([]);
|
||||||
|
|
||||||
final AlbumService _sharedAlbumService;
|
final AlbumService _sharedAlbumService;
|
||||||
final SharedAlbumCacheService _sharedAlbumCacheService;
|
final SharedAlbumCacheService _sharedAlbumCacheService;
|
||||||
@@ -16,7 +18,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
|||||||
|
|
||||||
Future<AlbumResponseDto?> createSharedAlbum(
|
Future<AlbumResponseDto?> createSharedAlbum(
|
||||||
String albumName,
|
String albumName,
|
||||||
Set<AssetResponseDto> assets,
|
Set<Asset> assets,
|
||||||
List<String> sharedUserIds,
|
List<String> sharedUserIds,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -29,7 +30,7 @@ class AlbumService {
|
|||||||
|
|
||||||
Future<AlbumResponseDto?> createAlbum(
|
Future<AlbumResponseDto?> createAlbum(
|
||||||
String albumName,
|
String albumName,
|
||||||
Set<AssetResponseDto> assets,
|
Iterable<Asset> assets,
|
||||||
List<String> sharedUserIds,
|
List<String> sharedUserIds,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
@@ -65,7 +66,7 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<AlbumResponseDto?> createAlbumWithGeneratedName(
|
Future<AlbumResponseDto?> createAlbumWithGeneratedName(
|
||||||
Set<AssetResponseDto> assets,
|
Iterable<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
return createAlbum(
|
return createAlbum(
|
||||||
_getNextAlbumName(await getAlbums(isShared: false)), assets, []);
|
_getNextAlbumName(await getAlbums(isShared: false)), assets, []);
|
||||||
@@ -81,7 +82,7 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
|
Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
|
||||||
Set<AssetResponseDto> assets,
|
Iterable<Asset> assets,
|
||||||
String albumId,
|
String albumId,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
|
||||||
class AlbumViewerThumbnail extends HookConsumerWidget {
|
class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
final List<AssetResponseDto> assetList;
|
final List<Asset> assetList;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
|
|
||||||
const AlbumViewerThumbnail({
|
const AlbumViewerThumbnail({
|
||||||
@@ -24,8 +21,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
final selectedAssetsInAlbumViewer =
|
final selectedAssetsInAlbumViewer =
|
||||||
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
@@ -120,27 +115,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
_buildThumbnailImage() {
|
_buildThumbnailImage() {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(border: drawBorderColor()),
|
decoration: BoxDecoration(border: drawBorderColor()),
|
||||||
child: CachedNetworkImage(
|
child: ImmichImage(asset, width: 300, height: 300),
|
||||||
cacheKey: asset.id,
|
|
||||||
width: 300,
|
|
||||||
height: 300,
|
|
||||||
memCacheHeight: 200,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: thumbnailRequestUrl,
|
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
|
||||||
Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +142,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
_buildThumbnailImage(),
|
_buildThumbnailImage(),
|
||||||
if (showStorageIndicator) _buildAssetStoreLocationIcon(),
|
if (showStorageIndicator) _buildAssetStoreLocationIcon(),
|
||||||
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
|
if (!asset.isImage) _buildVideoLabel(),
|
||||||
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
|
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class AssetGridByMonth extends HookConsumerWidget {
|
class AssetGridByMonth extends HookConsumerWidget {
|
||||||
final List<AssetResponseDto> assetGroup;
|
final List<Asset> assetGroup;
|
||||||
const AssetGridByMonth({Key? key, required this.assetGroup})
|
const AssetGridByMonth({Key? key, required this.assetGroup})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class MonthGroupTitle extends HookConsumerWidget {
|
class MonthGroupTitle extends HookConsumerWidget {
|
||||||
final String month;
|
final String month;
|
||||||
final List<AssetResponseDto> assetGroup;
|
final List<Asset> assetGroup;
|
||||||
|
|
||||||
const MonthGroupTitle({
|
const MonthGroupTitle({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
|
||||||
class SelectionThumbnailImage extends HookConsumerWidget {
|
class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
const SelectionThumbnailImage({Key? key, required this.asset})
|
const SelectionThumbnailImage({Key? key, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
|
||||||
var selectedAsset =
|
var selectedAsset =
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||||
var newAssetsForAlbum =
|
var newAssetsForAlbum =
|
||||||
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
||||||
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||||
|
|
||||||
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
Widget _buildSelectionIcon(Asset asset) {
|
||||||
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
var isNewlySelected =
|
var isNewlySelected =
|
||||||
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||||
@@ -110,30 +105,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(border: drawBorderColor()),
|
decoration: BoxDecoration(border: drawBorderColor()),
|
||||||
child: CachedNetworkImage(
|
child: ImmichImage(asset, width: 150, height: 150),
|
||||||
cacheKey: asset.id,
|
|
||||||
width: 150,
|
|
||||||
height: 150,
|
|
||||||
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: thumbnailRequestUrl,
|
|
||||||
httpHeaders: {
|
|
||||||
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
|
||||||
},
|
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
|
||||||
Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child:
|
|
||||||
CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(3.0),
|
padding: const EdgeInsets.all(3.0),
|
||||||
@@ -142,7 +114,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
child: _buildSelectionIcon(asset),
|
child: _buildSelectionIcon(asset),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (asset.type != AssetTypeEnum.IMAGE)
|
if (!asset.isImage)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
right: 5,
|
right: 5,
|
||||||
|
|||||||
@@ -1,49 +1,23 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
const SharedAlbumThumbnailImage({Key? key, required this.asset})
|
const SharedAlbumThumbnailImage({Key? key, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// debugPrint("View ${asset.id}");
|
// debugPrint("View ${asset.id}");
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
CachedNetworkImage(
|
ImmichImage(asset, width: 500, height: 500),
|
||||||
cacheKey: asset.id,
|
|
||||||
width: 500,
|
|
||||||
height: 500,
|
|
||||||
memCacheHeight: 500,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: getThumbnailUrl(asset),
|
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
|
||||||
Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child:
|
|
||||||
CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
|
|||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
@@ -38,9 +39,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
/// If they exist, add to selected asset state to show they are already selected.
|
/// If they exist, add to selected asset state to show they are already selected.
|
||||||
void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
|
void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
|
||||||
if (albumInfo.assets.isNotEmpty == true) {
|
if (albumInfo.assets.isNotEmpty == true) {
|
||||||
ref
|
ref.watch(assetSelectionProvider.notifier).addNewAssets(
|
||||||
.watch(assetSelectionProvider.notifier)
|
albumInfo.assets.map((e) => Asset.remote(e)).toList(),
|
||||||
.addNewAssets(albumInfo.assets.toList());
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
||||||
@@ -205,8 +206,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return AlbumViewerThumbnail(
|
return AlbumViewerThumbnail(
|
||||||
asset: albumInfo.assets[index],
|
asset: Asset.remote(albumInfo.assets[index]),
|
||||||
assetList: albumInfo.assets,
|
assetList:
|
||||||
|
albumInfo.assets.map((e) => Asset.remote(e)).toList(),
|
||||||
showStorageIndicator: showStorageIndicator,
|
showStorageIndicator: showStorageIndicator,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _onBackgroundTapped,
|
onTap: _onBackgroundTapped,
|
||||||
child: SharedAlbumThumbnailImage(
|
child: SharedAlbumThumbnailImage(
|
||||||
asset: selectedAssets.toList()[index],
|
asset: selectedAssets.elementAt(index),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
|
||||||
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/services/share.service.dart';
|
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||||
@@ -47,7 +47,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
|||||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void shareAsset(AssetResponseDto asset, BuildContext context) async {
|
void shareAsset(Asset asset, BuildContext context) async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext buildContext) {
|
builder: (BuildContext buildContext) {
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
class ExifBottomSheet extends ConsumerWidget {
|
class ExifBottomSheet extends ConsumerWidget {
|
||||||
final AssetResponseDto assetDetail;
|
final Asset assetDetail;
|
||||||
|
|
||||||
const ExifBottomSheet({Key? key, required this.assetDetail})
|
const ExifBottomSheet({Key? key, required this.assetDetail})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -26,8 +27,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
center: LatLng(
|
center: LatLng(
|
||||||
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
assetDetail.latitude ?? 0,
|
||||||
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
assetDetail.longitude ?? 0,
|
||||||
),
|
),
|
||||||
zoom: 16.0,
|
zoom: 16.0,
|
||||||
),
|
),
|
||||||
@@ -48,8 +49,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
Marker(
|
Marker(
|
||||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
point: LatLng(
|
point: LatLng(
|
||||||
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
assetDetail.latitude ?? 0,
|
||||||
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
assetDetail.longitude ?? 0,
|
||||||
),
|
),
|
||||||
builder: (ctx) => const Image(
|
builder: (ctx) => const Image(
|
||||||
image: AssetImage('assets/location-pin.png'),
|
image: AssetImage('assets/location-pin.png'),
|
||||||
@@ -63,9 +64,11 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo;
|
||||||
|
|
||||||
_buildLocationText() {
|
_buildLocationText() {
|
||||||
return Text(
|
return Text(
|
||||||
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
"${exifInfo?.city}, ${exifInfo?.state}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
@@ -78,10 +81,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
if (exifInfo?.dateTimeOriginal != null)
|
||||||
Text(
|
Text(
|
||||||
DateFormat('date_format'.tr()).format(
|
DateFormat('date_format'.tr()).format(
|
||||||
assetDetail.exifInfo!.dateTimeOriginal!.toLocal(),
|
exifInfo!.dateTimeOriginal!.toLocal(),
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[400],
|
color: Colors.grey[400],
|
||||||
@@ -101,7 +104,7 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
if (assetDetail.exifInfo?.latitude != null)
|
if (assetDetail.latitude != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -115,21 +118,22 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
"exif_bottom_sheet_location",
|
"exif_bottom_sheet_location",
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||||
).tr(),
|
).tr(),
|
||||||
if (assetDetail.exifInfo?.latitude != null &&
|
if (assetDetail.latitude != null &&
|
||||||
assetDetail.exifInfo?.longitude != null)
|
assetDetail.longitude != null)
|
||||||
_buildMap(),
|
_buildMap(),
|
||||||
if (assetDetail.exifInfo?.city != null &&
|
if (exifInfo != null &&
|
||||||
assetDetail.exifInfo?.state != null)
|
exifInfo.city != null &&
|
||||||
|
exifInfo.state != null)
|
||||||
_buildLocationText(),
|
_buildLocationText(),
|
||||||
Text(
|
Text(
|
||||||
"${assetDetail.exifInfo?.latitude?.toStringAsFixed(4)}, ${assetDetail.exifInfo?.longitude?.toStringAsFixed(4)}",
|
"${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}",
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Detail
|
// Detail
|
||||||
if (assetDetail.exifInfo != null)
|
if (exifInfo != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -153,16 +157,16 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
iconColor: Colors.grey[300],
|
iconColor: Colors.grey[300],
|
||||||
leading: const Icon(Icons.image),
|
leading: const Icon(Icons.image),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
|
"${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
subtitle: exifInfo.exifImageHeight != null
|
||||||
? Text(
|
? Text(
|
||||||
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ",
|
"${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${exifInfo.fileSizeInByte!}B ",
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
if (assetDetail.exifInfo?.make != null)
|
if (exifInfo.make != null)
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
dense: true,
|
dense: true,
|
||||||
@@ -170,11 +174,11 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
iconColor: Colors.grey[300],
|
iconColor: Colors.grey[300],
|
||||||
leading: const Icon(Icons.camera),
|
leading: const Icon(Icons.camera),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
|
"${exifInfo.make} ${exifInfo.model}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} ",
|
"ƒ/${exifInfo.fNumber} 1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)} ${exifInfo.focalLength}mm ISO${exifInfo.iso} ",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart'
|
||||||
|
show AssetEntityImageProvider, ThumbnailSize;
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
enum _RemoteImageStatus { empty, thumbnail, preview, full }
|
enum _RemoteImageStatus { empty, thumbnail, preview, full }
|
||||||
|
|
||||||
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
late CachedNetworkImageProvider _imageProvider;
|
late ImageProvider _imageProvider;
|
||||||
_RemoteImageStatus _status = _RemoteImageStatus.empty;
|
_RemoteImageStatus _status = _RemoteImageStatus.empty;
|
||||||
bool _zoomedIn = false;
|
bool _zoomedIn = false;
|
||||||
|
|
||||||
late CachedNetworkImageProvider fullProvider;
|
late ImageProvider _fullProvider;
|
||||||
late CachedNetworkImageProvider previewProvider;
|
late ImageProvider _previewProvider;
|
||||||
late CachedNetworkImageProvider thumbnailProvider;
|
late ImageProvider _thumbnailProvider;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -68,7 +73,7 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
|
|
||||||
void _performStateTransition(
|
void _performStateTransition(
|
||||||
_RemoteImageStatus newStatus,
|
_RemoteImageStatus newStatus,
|
||||||
CachedNetworkImageProvider provider,
|
ImageProvider provider,
|
||||||
) {
|
) {
|
||||||
if (_status == newStatus) return;
|
if (_status == newStatus) return;
|
||||||
|
|
||||||
@@ -90,40 +95,58 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _loadImages() {
|
void _loadImages() {
|
||||||
thumbnailProvider = _authorizedImageProvider(
|
if (widget.asset.isLocal) {
|
||||||
widget.thumbnailUrl,
|
_imageProvider = AssetEntityImageProvider(
|
||||||
widget.cacheKey,
|
widget.asset.local!,
|
||||||
);
|
isOriginal: false,
|
||||||
_imageProvider = thumbnailProvider;
|
thumbnailSize: const ThumbnailSize.square(250),
|
||||||
|
);
|
||||||
|
_fullProvider = AssetEntityImageProvider(widget.asset.local!);
|
||||||
|
_fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
|
ImageStreamListener((ImageInfo image, _) {
|
||||||
|
_performStateTransition(
|
||||||
|
_RemoteImageStatus.full,
|
||||||
|
_fullProvider,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
_thumbnailProvider = _authorizedImageProvider(
|
||||||
|
getThumbnailUrl(widget.asset.remote!),
|
||||||
|
widget.asset.id,
|
||||||
|
);
|
||||||
|
_imageProvider = _thumbnailProvider;
|
||||||
|
|
||||||
|
_thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
_performStateTransition(
|
_performStateTransition(
|
||||||
_RemoteImageStatus.thumbnail,
|
_RemoteImageStatus.thumbnail,
|
||||||
thumbnailProvider,
|
_thumbnailProvider,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.previewUrl != null) {
|
if (widget.threeStageLoading) {
|
||||||
previewProvider = _authorizedImageProvider(
|
_previewProvider = _authorizedImageProvider(
|
||||||
widget.previewUrl!,
|
getThumbnailUrl(widget.asset.remote!, type: ThumbnailFormat.JPEG),
|
||||||
"${widget.cacheKey}_previewStage",
|
"${widget.asset.id}_previewStage",
|
||||||
);
|
);
|
||||||
previewProvider.resolve(const ImageConfiguration()).addListener(
|
_previewProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
_performStateTransition(_RemoteImageStatus.preview, previewProvider);
|
_performStateTransition(_RemoteImageStatus.preview, _previewProvider);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fullProvider = _authorizedImageProvider(
|
_fullProvider = _authorizedImageProvider(
|
||||||
widget.imageUrl,
|
getImageUrl(widget.asset.remote!),
|
||||||
"${widget.cacheKey}_fullStage",
|
"${widget.asset.id}_fullStage",
|
||||||
);
|
);
|
||||||
fullProvider.resolve(const ImageConfiguration()).addListener(
|
_fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
ImageStreamListener((ImageInfo imageInfo, _) {
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
_performStateTransition(_RemoteImageStatus.full, _fullProvider);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -139,11 +162,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
||||||
if (_status == _RemoteImageStatus.full) {
|
if (_status == _RemoteImageStatus.full) {
|
||||||
await fullProvider.evict();
|
await _fullProvider.evict();
|
||||||
} else if (_status == _RemoteImageStatus.preview) {
|
} else if (_status == _RemoteImageStatus.preview) {
|
||||||
await previewProvider.evict();
|
await _previewProvider.evict();
|
||||||
} else if (_status == _RemoteImageStatus.thumbnail) {
|
} else if (_status == _RemoteImageStatus.thumbnail) {
|
||||||
await thumbnailProvider.evict();
|
await _thumbnailProvider.evict();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _imageProvider.evict();
|
await _imageProvider.evict();
|
||||||
@@ -153,23 +176,18 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
class RemotePhotoView extends StatefulWidget {
|
class RemotePhotoView extends StatefulWidget {
|
||||||
const RemotePhotoView({
|
const RemotePhotoView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.thumbnailUrl,
|
required this.asset,
|
||||||
required this.imageUrl,
|
|
||||||
required this.authToken,
|
required this.authToken,
|
||||||
|
required this.threeStageLoading,
|
||||||
required this.isZoomedFunction,
|
required this.isZoomedFunction,
|
||||||
required this.isZoomedListener,
|
required this.isZoomedListener,
|
||||||
required this.onSwipeDown,
|
required this.onSwipeDown,
|
||||||
required this.onSwipeUp,
|
required this.onSwipeUp,
|
||||||
this.previewUrl,
|
|
||||||
required this.cacheKey,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String thumbnailUrl;
|
final Asset asset;
|
||||||
final String imageUrl;
|
|
||||||
final String authToken;
|
final String authToken;
|
||||||
final String? previewUrl;
|
final bool threeStageLoading;
|
||||||
final String cacheKey;
|
|
||||||
|
|
||||||
final void Function() onSwipeDown;
|
final void Function() onSwipeDown;
|
||||||
final void Function() onSwipeUp;
|
final void Function() onSwipeUp;
|
||||||
final void Function() isZoomedFunction;
|
final void Function() isZoomedFunction;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
const TopControlAppBar({
|
const TopControlAppBar({
|
||||||
@@ -13,9 +13,9 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
this.loading = false,
|
this.loading = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
final Function onDownloadPressed;
|
final VoidCallback? onDownloadPressed;
|
||||||
final Function onSharePressed;
|
final Function onSharePressed;
|
||||||
final bool loading;
|
final bool loading;
|
||||||
|
|
||||||
@@ -47,17 +47,16 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
child: const CircularProgressIndicator(strokeWidth: 2.0),
|
child: const CircularProgressIndicator(strokeWidth: 2.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
if (!asset.isLocal)
|
||||||
iconSize: iconSize,
|
IconButton(
|
||||||
splashRadius: iconSize,
|
iconSize: iconSize,
|
||||||
onPressed: () {
|
splashRadius: iconSize,
|
||||||
onDownloadPressed();
|
onPressed: onDownloadPressed,
|
||||||
},
|
icon: Icon(
|
||||||
icon: Icon(
|
Icons.cloud_download_rounded,
|
||||||
Icons.cloud_download_rounded,
|
color: Colors.grey[200],
|
||||||
color: Colors.grey[200],
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
@@ -69,17 +68,18 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
if (asset.isRemote)
|
||||||
iconSize: iconSize,
|
IconButton(
|
||||||
splashRadius: iconSize,
|
iconSize: iconSize,
|
||||||
onPressed: () {
|
splashRadius: iconSize,
|
||||||
onMoreInfoPressed();
|
onPressed: () {
|
||||||
},
|
onMoreInfoPressed();
|
||||||
icon: Icon(
|
},
|
||||||
Icons.more_horiz_rounded,
|
icon: Icon(
|
||||||
color: Colors.grey[200],
|
Icons.more_horiz_rounded,
|
||||||
),
|
color: Colors.grey[200],
|
||||||
)
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'
|
|||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class GalleryViewerPage extends HookConsumerWidget {
|
class GalleryViewerPage extends HookConsumerWidget {
|
||||||
late List<AssetResponseDto> assetList;
|
late List<Asset> assetList;
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
GalleryViewerPage({
|
GalleryViewerPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -27,7 +27,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
required this.asset,
|
required this.asset,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AssetResponseDto? assetDetail;
|
Asset? assetDetail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -37,8 +37,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final loading = useState(false);
|
final loading = useState(false);
|
||||||
final isZoomed = useState<bool>(false);
|
final isZoomed = useState<bool>(false);
|
||||||
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
|
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
|
||||||
|
final indexOfAsset = useState(assetList.indexOf(asset));
|
||||||
int indexOfAsset = assetList.indexOf(asset);
|
|
||||||
|
|
||||||
PageController controller =
|
PageController controller =
|
||||||
PageController(initialPage: assetList.indexOf(asset));
|
PageController(initialPage: assetList.indexOf(asset));
|
||||||
@@ -52,15 +51,15 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
|
||||||
initState(int index) {
|
|
||||||
indexOfAsset = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail = await ref
|
if (assetList[indexOfAsset.value].isRemote) {
|
||||||
.watch(assetServiceProvider)
|
assetDetail = await ref
|
||||||
.getAssetById(assetList[indexOfAsset].id);
|
.watch(assetServiceProvider)
|
||||||
|
.getAssetById(assetList[indexOfAsset.value].id);
|
||||||
|
} else {
|
||||||
|
// TODO local exif parsing?
|
||||||
|
assetDetail = assetList[indexOfAsset.value];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showInfo() {
|
void showInfo() {
|
||||||
@@ -88,19 +87,20 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
appBar: TopControlAppBar(
|
appBar: TopControlAppBar(
|
||||||
loading: loading.value,
|
loading: loading.value,
|
||||||
asset: assetList[indexOfAsset],
|
asset: assetList[indexOfAsset.value],
|
||||||
onMoreInfoPressed: () {
|
onMoreInfoPressed: () {
|
||||||
showInfo();
|
showInfo();
|
||||||
},
|
},
|
||||||
onDownloadPressed: () {
|
onDownloadPressed: assetList[indexOfAsset.value].isLocal
|
||||||
ref
|
? null
|
||||||
.watch(imageViewerStateProvider.notifier)
|
: () {
|
||||||
.downloadAsset(assetList[indexOfAsset], context);
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||||
},
|
assetList[indexOfAsset.value].remote!, context);
|
||||||
|
},
|
||||||
onSharePressed: () {
|
onSharePressed: () {
|
||||||
ref
|
ref
|
||||||
.watch(imageViewerStateProvider.notifier)
|
.watch(imageViewerStateProvider.notifier)
|
||||||
.shareAsset(assetList[indexOfAsset], context);
|
.shareAsset(assetList[indexOfAsset.value], context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
@@ -113,14 +113,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
itemCount: assetList.length,
|
itemCount: assetList.length,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
onPageChanged: (value) {
|
onPageChanged: (value) {
|
||||||
|
indexOfAsset.value = value;
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
},
|
},
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
initState(index);
|
|
||||||
|
|
||||||
getAssetExif();
|
getAssetExif();
|
||||||
|
|
||||||
if (assetList[index].type == AssetTypeEnum.IMAGE) {
|
if (assetList[index].isImage) {
|
||||||
return ImageViewerPage(
|
return ImageViewerPage(
|
||||||
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
authToken: 'Bearer ${box.get(accessTokenKey)}',
|
||||||
isZoomedFunction: isZoomedMethod,
|
isZoomedFunction: isZoomedMethod,
|
||||||
@@ -139,11 +138,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: assetList[index].id,
|
tag: assetList[index].id,
|
||||||
child: VideoViewerPage(
|
child: VideoViewerPage(asset: assetList[index]),
|
||||||
asset: assetList[index],
|
|
||||||
videoUrl:
|
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class ImageViewerPage extends HookConsumerWidget {
|
class ImageViewerPage extends HookConsumerWidget {
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
final String authToken;
|
final String authToken;
|
||||||
final ValueNotifier<bool> isZoomedListener;
|
final ValueNotifier<bool> isZoomedListener;
|
||||||
final void Function() isZoomedFunction;
|
final void Function() isZoomedFunction;
|
||||||
@@ -30,7 +29,7 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
required this.threeStageLoading,
|
required this.threeStageLoading,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AssetResponseDto? assetDetail;
|
Asset? assetDetail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -38,8 +37,13 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail =
|
if (asset.isRemote) {
|
||||||
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
assetDetail =
|
||||||
|
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
||||||
|
} else {
|
||||||
|
// TODO local exif parsing?
|
||||||
|
assetDetail = asset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -68,17 +72,13 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
child: Hero(
|
child: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: RemotePhotoView(
|
child: RemotePhotoView(
|
||||||
thumbnailUrl: getThumbnailUrl(asset),
|
asset: asset,
|
||||||
cacheKey: asset.id,
|
|
||||||
imageUrl: getImageUrl(asset),
|
|
||||||
previewUrl: threeStageLoading
|
|
||||||
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
|
|
||||||
: null,
|
|
||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
|
threeStageLoading: threeStageLoading,
|
||||||
isZoomedFunction: isZoomedFunction,
|
isZoomedFunction: isZoomedFunction,
|
||||||
isZoomedListener: isZoomedListener,
|
isZoomedListener: isZoomedListener,
|
||||||
onSwipeDown: () => AutoRouter.of(context).pop(),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
onSwipeUp: () => showInfo(),
|
onSwipeUp: asset.isRemote ? showInfo : () {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -6,24 +8,41 @@ import 'package:chewie/chewie.dart';
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class VideoViewerPage extends HookConsumerWidget {
|
class VideoViewerPage extends HookConsumerWidget {
|
||||||
final String videoUrl;
|
final Asset asset;
|
||||||
final AssetResponseDto asset;
|
|
||||||
AssetResponseDto? assetDetail;
|
|
||||||
|
|
||||||
VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
|
const VideoViewerPage({Key? key, required this.asset}) : super(key: key);
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
if (asset.isLocal) {
|
||||||
|
final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!));
|
||||||
|
return videoFile.when(
|
||||||
|
data: (data) => VideoThumbnailPlayer(file: data),
|
||||||
|
error: (error, stackTrace) => Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
loading: () => const Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 75,
|
||||||
|
height: 75,
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
final downloadAssetStatus =
|
final downloadAssetStatus =
|
||||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
|
final box = Hive.box(userInfoBox);
|
||||||
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
final String jwtToken = box.get(accessTokenKey);
|
||||||
|
final String videoUrl =
|
||||||
|
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}';
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -40,11 +59,21 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoThumbnailPlayer extends StatefulWidget {
|
final _fileFamily =
|
||||||
final String url;
|
FutureProvider.family<File, AssetEntity>((ref, entity) async {
|
||||||
final String? jwtToken;
|
final file = await entity.file;
|
||||||
|
if (file == null) {
|
||||||
|
throw Exception();
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
|
||||||
const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken})
|
class VideoThumbnailPlayer extends StatefulWidget {
|
||||||
|
final String? url;
|
||||||
|
final String? jwtToken;
|
||||||
|
final File? file;
|
||||||
|
|
||||||
|
const VideoThumbnailPlayer({Key? key, this.url, this.jwtToken, this.file})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -63,10 +92,12 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
|||||||
|
|
||||||
Future<void> initializePlayer() async {
|
Future<void> initializePlayer() async {
|
||||||
try {
|
try {
|
||||||
videoPlayerController = VideoPlayerController.network(
|
videoPlayerController = widget.file == null
|
||||||
widget.url,
|
? VideoPlayerController.network(
|
||||||
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
widget.url!,
|
||||||
);
|
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
||||||
|
)
|
||||||
|
: VideoPlayerController.file(widget.file!);
|
||||||
|
|
||||||
await videoPlayerController.initialize();
|
await videoPlayerController.initialize();
|
||||||
_createChewieController();
|
_createChewieController();
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ class BackgroundService {
|
|||||||
_Throttle(_updateProgress, notifyInterval);
|
_Throttle(_updateProgress, notifyInterval);
|
||||||
late final _Throttle _throttledDetailNotify =
|
late final _Throttle _throttledDetailNotify =
|
||||||
_Throttle(_updateDetailProgress, notifyInterval);
|
_Throttle(_updateDetailProgress, notifyInterval);
|
||||||
|
Completer<bool> _hasAccessCompleter = Completer();
|
||||||
|
late Future<bool> _hasAccess =
|
||||||
|
Platform.isAndroid ? _hasAccessCompleter.future : Future.value(true);
|
||||||
|
|
||||||
|
Future<bool> get hasAccess => _hasAccess;
|
||||||
|
|
||||||
bool get isBackgroundInitialized {
|
bool get isBackgroundInitialized {
|
||||||
return _isBackgroundInitialized;
|
return _isBackgroundInitialized;
|
||||||
@@ -201,6 +206,15 @@ class BackgroundService {
|
|||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (_hasLock) {
|
||||||
|
debugPrint("WARNING: [acquireLock] called more than once");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_hasAccessCompleter.isCompleted) {
|
||||||
|
debugPrint("WARNING: [acquireLock] _hasAccessCompleter is completed");
|
||||||
|
_hasAccessCompleter = Completer();
|
||||||
|
_hasAccess = _hasAccessCompleter.future;
|
||||||
|
}
|
||||||
final int lockTime = Timeline.now;
|
final int lockTime = Timeline.now;
|
||||||
_wantsLockTime = lockTime;
|
_wantsLockTime = lockTime;
|
||||||
final ReceivePort rp = ReceivePort(_portNameLock);
|
final ReceivePort rp = ReceivePort(_portNameLock);
|
||||||
@@ -219,6 +233,7 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
_hasLock = true;
|
_hasLock = true;
|
||||||
rp.listen(_heartbeatListener);
|
rp.listen(_heartbeatListener);
|
||||||
|
_hasAccessCompleter.complete(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +286,8 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
_wantsLockTime = 0;
|
_wantsLockTime = 0;
|
||||||
if (_hasLock) {
|
if (_hasLock) {
|
||||||
|
_hasAccessCompleter = Completer();
|
||||||
|
_hasAccess = _hasAccessCompleter.future;
|
||||||
IsolateNameServer.removePortNameMapping(_portNameLock);
|
IsolateNameServer.removePortNameMapping(_portNameLock);
|
||||||
_waitingIsolate?.send(true);
|
_waitingIsolate?.send(true);
|
||||||
_waitingIsolate = null;
|
_waitingIsolate = null;
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ class HiveBackupAlbums {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a deep copy to allow safe modification without changing the global
|
||||||
|
/// state of [HiveBackupAlbums] before actually saving the changes
|
||||||
|
HiveBackupAlbums deepCopy() {
|
||||||
|
return HiveBackupAlbums(
|
||||||
|
selectedAlbumIds: selectedAlbumIds.toList(),
|
||||||
|
excludedAlbumsIds: excludedAlbumsIds.toList(),
|
||||||
|
lastSelectedBackupTime: lastSelectedBackupTime.toList(),
|
||||||
|
lastExcludedBackupTime: lastExcludedBackupTime.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
final result = <String, dynamic>{};
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
|||||||
@@ -565,11 +565,16 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
||||||
final bool hasLock = await _backgroundService.acquireLock();
|
final bool hasLock = await _backgroundService.acquireLock();
|
||||||
if (!hasLock) {
|
if (!hasLock) {
|
||||||
|
debugPrint("WARNING [resumeBackup] failed to acquireLock");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Box<HiveBackupAlbums> box =
|
await Future.wait([
|
||||||
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
||||||
HiveBackupAlbums? albums = box.get(backupInfoKey);
|
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
||||||
|
Hive.openBox(backgroundBackupInfoBox),
|
||||||
|
]);
|
||||||
|
final HiveBackupAlbums? albums =
|
||||||
|
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
|
||||||
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||||
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||||
if (albums != null) {
|
if (albums != null) {
|
||||||
@@ -584,8 +589,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
albums.lastExcludedBackupTime,
|
albums.lastExcludedBackupTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
|
final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
|
||||||
final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: previous,
|
backupProgress: previous,
|
||||||
selectedBackupAlbums: selectedAlbums,
|
selectedBackupAlbums: selectedAlbums,
|
||||||
|
|||||||
@@ -1,34 +1,90 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/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/services/backup.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:photo_manager/src/types/entity.dart';
|
||||||
|
|
||||||
final assetServiceProvider = Provider(
|
final assetServiceProvider = Provider(
|
||||||
(ref) => AssetService(
|
(ref) => AssetService(
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
|
ref.watch(backupServiceProvider),
|
||||||
|
ref.watch(backgroundServiceProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
|
final BackupService _backupService;
|
||||||
|
final BackgroundService _backgroundService;
|
||||||
|
|
||||||
AssetService(this._apiService);
|
AssetService(this._apiService, this._backupService, this._backgroundService);
|
||||||
|
|
||||||
Future<List<AssetResponseDto>?> getAllAsset() async {
|
/// Returns all local, remote assets in that order
|
||||||
|
Future<List<Asset>> getAllAsset({bool urgent = false}) async {
|
||||||
|
final List<Asset> assets = [];
|
||||||
try {
|
try {
|
||||||
return await _apiService.assetApi.getAllAssets();
|
// not using `await` here to fetch local & remote assets concurrently
|
||||||
|
final Future<List<AssetResponseDto>?> remoteTask =
|
||||||
|
_apiService.assetApi.getAllAssets();
|
||||||
|
final Iterable<AssetEntity> newLocalAssets;
|
||||||
|
final List<AssetEntity> localAssets = await _getLocalAssets(urgent);
|
||||||
|
final List<AssetResponseDto> remoteAssets = await remoteTask ?? [];
|
||||||
|
if (remoteAssets.isNotEmpty && localAssets.isNotEmpty) {
|
||||||
|
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
final Set<String> existingIds = remoteAssets
|
||||||
|
.where((e) => e.deviceId == deviceId)
|
||||||
|
.map((e) => e.deviceAssetId)
|
||||||
|
.toSet();
|
||||||
|
newLocalAssets = localAssets.where((e) => !existingIds.contains(e.id));
|
||||||
|
} else {
|
||||||
|
newLocalAssets = localAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
assets.addAll(newLocalAssets.map((e) => Asset.local(e)));
|
||||||
|
// the order (first all local, then remote assets) is important!
|
||||||
|
assets.addAll(remoteAssets.map((e) => Asset.remote(e)));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getAllAsset] ${e.toString()}");
|
debugPrint("Error [getAllAsset] ${e.toString()}");
|
||||||
return null;
|
}
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// if [urgent] is `true`, do not block by waiting on the background service
|
||||||
|
/// to finish running. Returns an empty list instead after a timeout.
|
||||||
|
Future<List<AssetEntity>> _getLocalAssets(bool urgent) async {
|
||||||
|
try {
|
||||||
|
final Future<bool> hasAccess = urgent
|
||||||
|
? _backgroundService.hasAccess
|
||||||
|
.timeout(const Duration(milliseconds: 250))
|
||||||
|
: _backgroundService.hasAccess;
|
||||||
|
if (!await hasAccess) {
|
||||||
|
throw Exception("Error [getAllAsset] failed to gain access");
|
||||||
|
}
|
||||||
|
final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
|
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
||||||
|
|
||||||
|
return backupAlbumInfo != null
|
||||||
|
? await _backupService
|
||||||
|
.buildUploadCandidates(backupAlbumInfo.deepCopy())
|
||||||
|
: [];
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error [_getLocalAssets] ${e.toString()}");
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AssetResponseDto?> getAssetById(String assetId) async {
|
Future<Asset?> getAssetById(String assetId) async {
|
||||||
try {
|
try {
|
||||||
return await _apiService.assetApi.getAssetById(assetId);
|
return Asset.remote(await _apiService.assetApi.getAssetById(assetId));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getAssetById] ${e.toString()}");
|
debugPrint("Error [getAssetById] ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
@@ -36,12 +92,12 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
||||||
Set<AssetResponseDto> deleteAssets,
|
Iterable<AssetResponseDto> deleteAssets,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
List<String> payload = [];
|
final List<String> payload = [];
|
||||||
|
|
||||||
for (var asset in deleteAssets) {
|
for (final asset in deleteAssets) {
|
||||||
payload.add(asset.id);
|
payload.add(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/services/json_cache.dart';
|
import 'package:immich_mobile/shared/services/json_cache.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
|
class AssetCacheService extends JsonCache<List<Asset>> {
|
||||||
class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
|
|
||||||
AssetCacheService() : super("asset_cache");
|
AssetCacheService() : super("asset_cache");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void put(List<AssetResponseDto> data) {
|
void put(List<Asset> data) {
|
||||||
putRawData(data.map((e) => e.toJson()).toList());
|
putRawData(data.map((e) => e.toJson()).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<AssetResponseDto>> get() async {
|
Future<List<Asset>> get() async {
|
||||||
try {
|
try {
|
||||||
final mapList = await readRawData() as List<dynamic>;
|
final mapList = await readRawData() as List<dynamic>;
|
||||||
|
|
||||||
final responseData = mapList
|
final responseData =
|
||||||
.map((e) => AssetResponseDto.fromJson(e))
|
mapList.map((e) => Asset.fromJson(e)).whereNotNull().toList();
|
||||||
.whereNotNull()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return responseData;
|
return responseData;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -33,5 +30,5 @@ class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final assetCacheServiceProvider = Provider(
|
final assetCacheServiceProvider = Provider(
|
||||||
(ref) => AssetCacheService(),
|
(ref) => AssetCacheService(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
enum RenderAssetGridElementType {
|
enum RenderAssetGridElementType {
|
||||||
assetRow,
|
assetRow,
|
||||||
@@ -9,7 +9,7 @@ enum RenderAssetGridElementType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RenderAssetGridRow {
|
class RenderAssetGridRow {
|
||||||
final List<AssetResponseDto> assets;
|
final List<Asset> assets;
|
||||||
|
|
||||||
RenderAssetGridRow(this.assets);
|
RenderAssetGridRow(this.assets);
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ class RenderAssetGridElement {
|
|||||||
final RenderAssetGridRow? assetRow;
|
final RenderAssetGridRow? assetRow;
|
||||||
final String? title;
|
final String? title;
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
final List<AssetResponseDto>? relatedAssetList;
|
final List<Asset>? relatedAssetList;
|
||||||
|
|
||||||
RenderAssetGridElement(
|
RenderAssetGridElement(
|
||||||
this.type, {
|
this.type, {
|
||||||
@@ -31,13 +31,15 @@ class RenderAssetGridElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<RenderAssetGridElement> assetsToRenderList(
|
List<RenderAssetGridElement> assetsToRenderList(
|
||||||
List<AssetResponseDto> assets, int assetsPerRow) {
|
List<Asset> assets,
|
||||||
|
int assetsPerRow,
|
||||||
|
) {
|
||||||
List<RenderAssetGridElement> elements = [];
|
List<RenderAssetGridElement> elements = [];
|
||||||
|
|
||||||
int cursor = 0;
|
int cursor = 0;
|
||||||
while (cursor < assets.length) {
|
while (cursor < assets.length) {
|
||||||
int rowElements = min(assets.length - cursor, assetsPerRow);
|
int rowElements = min(assets.length - cursor, assetsPerRow);
|
||||||
final date = DateTime.parse(assets[cursor].createdAt);
|
final date = assets[cursor].createdAt;
|
||||||
|
|
||||||
final rowElement = RenderAssetGridElement(
|
final rowElement = RenderAssetGridElement(
|
||||||
RenderAssetGridElementType.assetRow,
|
RenderAssetGridElementType.assetRow,
|
||||||
@@ -55,7 +57,9 @@ List<RenderAssetGridElement> assetsToRenderList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<RenderAssetGridElement> assetGroupsToRenderList(
|
List<RenderAssetGridElement> assetGroupsToRenderList(
|
||||||
Map<String, List<AssetResponseDto>> assetGroups, int assetsPerRow) {
|
Map<String, List<Asset>> assetGroups,
|
||||||
|
int assetsPerRow,
|
||||||
|
) {
|
||||||
List<RenderAssetGridElement> elements = [];
|
List<RenderAssetGridElement> elements = [];
|
||||||
DateTime? lastDate;
|
DateTime? lastDate;
|
||||||
|
|
||||||
@@ -64,8 +68,11 @@ List<RenderAssetGridElement> assetGroupsToRenderList(
|
|||||||
|
|
||||||
if (lastDate == null || lastDate!.month != date.month) {
|
if (lastDate == null || lastDate!.month != date.month) {
|
||||||
elements.add(
|
elements.add(
|
||||||
RenderAssetGridElement(RenderAssetGridElementType.monthTitle,
|
RenderAssetGridElement(
|
||||||
title: groupName, date: date),
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
title: groupName,
|
||||||
|
date: date,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'asset_grid_data_structure.dart';
|
import 'asset_grid_data_structure.dart';
|
||||||
import 'daily_title_text.dart';
|
import 'daily_title_text.dart';
|
||||||
@@ -13,7 +13,7 @@ import 'draggable_scrollbar_custom.dart';
|
|||||||
|
|
||||||
typedef ImmichAssetGridSelectionListener = void Function(
|
typedef ImmichAssetGridSelectionListener = void Function(
|
||||||
bool,
|
bool,
|
||||||
Set<AssetResponseDto>,
|
Set<Asset>,
|
||||||
);
|
);
|
||||||
|
|
||||||
class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
||||||
@@ -24,20 +24,20 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
bool _scrolling = false;
|
bool _scrolling = false;
|
||||||
final Set<String> _selectedAssets = HashSet();
|
final Set<String> _selectedAssets = HashSet();
|
||||||
|
|
||||||
List<AssetResponseDto> get _assets {
|
List<Asset> get _assets {
|
||||||
return widget.renderList
|
return widget.renderList
|
||||||
.map((e) {
|
.map((e) {
|
||||||
if (e.type == RenderAssetGridElementType.assetRow) {
|
if (e.type == RenderAssetGridElementType.assetRow) {
|
||||||
return e.assetRow!.assets;
|
return e.assetRow!.assets;
|
||||||
} else {
|
} else {
|
||||||
return List<AssetResponseDto>.empty();
|
return List<Asset>.empty();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flattened
|
.flattened
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<AssetResponseDto> _getSelectedAssets() {
|
Set<Asset> _getSelectedAssets() {
|
||||||
return _selectedAssets
|
return _selectedAssets
|
||||||
.map((e) => _assets.firstWhereOrNull((a) => a.id == e))
|
.map((e) => _assets.firstWhereOrNull((a) => a.id == e))
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
@@ -48,7 +48,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
widget.listener?.call(selectionActive, _getSelectedAssets());
|
widget.listener?.call(selectionActive, _getSelectedAssets());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectAssets(List<AssetResponseDto> assets) {
|
void _selectAssets(List<Asset> assets) {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (var e in assets) {
|
for (var e in assets) {
|
||||||
_selectedAssets.add(e.id);
|
_selectedAssets.add(e.id);
|
||||||
@@ -57,7 +57,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deselectAssets(List<AssetResponseDto> assets) {
|
void _deselectAssets(List<Asset> assets) {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (var e in assets) {
|
for (var e in assets) {
|
||||||
_selectedAssets.remove(e.id);
|
_selectedAssets.remove(e.id);
|
||||||
@@ -74,7 +74,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
_callSelectionListener(false);
|
_callSelectionListener(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _allAssetsSelected(List<AssetResponseDto> assets) {
|
bool _allAssetsSelected(List<Asset> assets) {
|
||||||
return widget.selectionActive &&
|
return widget.selectionActive &&
|
||||||
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThumbnailOrPlaceholder(
|
Widget _buildThumbnailOrPlaceholder(
|
||||||
AssetResponseDto asset,
|
Asset asset,
|
||||||
bool placeholder,
|
bool placeholder,
|
||||||
) {
|
) {
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
@@ -114,7 +114,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
key: Key("asset-row-${row.assets.first.id}"),
|
key: Key("asset-row-${row.assets.first.id}"),
|
||||||
children: row.assets.map((AssetResponseDto asset) {
|
children: row.assets.map((Asset asset) {
|
||||||
bool last = asset == row.assets.last;
|
bool last = asset == row.assets.last;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@@ -134,7 +134,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
Widget _buildTitle(
|
Widget _buildTitle(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String title,
|
String title,
|
||||||
List<AssetResponseDto> assets,
|
List<Asset> assets,
|
||||||
) {
|
) {
|
||||||
return DailyTitleText(
|
return DailyTitleText(
|
||||||
isoDate: title,
|
isoDate: title,
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
|
||||||
class ThumbnailImage extends HookConsumerWidget {
|
class ThumbnailImage extends HookConsumerWidget {
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
final List<AssetResponseDto> assetList;
|
final List<Asset> assetList;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final bool useGrayBoxPlaceholder;
|
final bool useGrayBoxPlaceholder;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
@@ -34,12 +31,9 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
|
||||||
var thumbnailRequestUrl = getThumbnailUrl(asset);
|
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
|
|
||||||
|
Widget buildSelectionIcon(Asset asset) {
|
||||||
Widget buildSelectionIcon(AssetResponseDto asset) {
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
@@ -87,41 +81,11 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: const Border(),
|
: const Border(),
|
||||||
),
|
),
|
||||||
child: CachedNetworkImage(
|
child: ImmichImage(
|
||||||
cacheKey: 'thumbnail-image-${asset.id}',
|
asset,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
memCacheHeight: 200,
|
useGrayBoxPlaceholder: useGrayBoxPlaceholder,
|
||||||
maxWidthDiskCache: 200,
|
|
||||||
maxHeightDiskCache: 200,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: thumbnailRequestUrl,
|
|
||||||
httpHeaders: {
|
|
||||||
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
|
||||||
},
|
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) {
|
|
||||||
if (useGrayBoxPlaceholder) {
|
|
||||||
return const DecoratedBox(
|
|
||||||
decoration: BoxDecoration(color: Colors.grey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
value: downloadProgress.progress,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
errorWidget: (context, url, error) {
|
|
||||||
debugPrint("Error getting thumbnail $url = $error");
|
|
||||||
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
|
|
||||||
|
|
||||||
return Icon(
|
|
||||||
Icons.image_not_supported_outlined,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (multiselectEnabled)
|
if (multiselectEnabled)
|
||||||
@@ -137,14 +101,16 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
right: 10,
|
right: 10,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
(deviceId != asset.deviceId)
|
asset.isRemote
|
||||||
? Icons.cloud_done_outlined
|
? (deviceId == asset.deviceId
|
||||||
: Icons.photo_library_rounded,
|
? Icons.cloud_done_outlined
|
||||||
|
: Icons.cloud_outlined)
|
||||||
|
: Icons.cloud_off_outlined,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (asset.type != AssetTypeEnum.IMAGE)
|
if (!asset.isImage)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 5,
|
top: 5,
|
||||||
right: 5,
|
right: 5,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
@@ -14,6 +15,7 @@ import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart
|
|||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
@@ -31,7 +33,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
|
||||||
final selectionEnabledHook = useState(false);
|
final selectionEnabledHook = useState(false);
|
||||||
|
|
||||||
final selection = useState(<AssetResponseDto>{});
|
final selection = useState(<Asset>{});
|
||||||
final albums = ref.watch(albumProvider);
|
final albums = ref.watch(albumProvider);
|
||||||
final albumService = ref.watch(albumServiceProvider);
|
final albumService = ref.watch(albumServiceProvider);
|
||||||
|
|
||||||
@@ -60,7 +62,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
Widget buildBody() {
|
Widget buildBody() {
|
||||||
void selectionListener(
|
void selectionListener(
|
||||||
bool multiselect,
|
bool multiselect,
|
||||||
Set<AssetResponseDto> selectedAssets,
|
Set<Asset> selectedAssets,
|
||||||
) {
|
) {
|
||||||
selectionEnabledHook.value = multiselect;
|
selectionEnabledHook.value = multiselect;
|
||||||
selection.value = selectedAssets;
|
selection.value = selectedAssets;
|
||||||
@@ -76,9 +78,27 @@ class HomePage extends HookConsumerWidget {
|
|||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Iterable<Asset> remoteOnlySelection() {
|
||||||
|
final Set<Asset> assets = selection.value;
|
||||||
|
final bool onlyRemote = assets.every((e) => e.isRemote);
|
||||||
|
if (!onlyRemote) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Can not add local assets to albums yet, skipping",
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
return assets.where((a) => a.isRemote);
|
||||||
|
}
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
void onAddToAlbum(AlbumResponseDto album) async {
|
void onAddToAlbum(AlbumResponseDto album) async {
|
||||||
|
final Iterable<Asset> assets = remoteOnlySelection();
|
||||||
|
if (assets.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final result = await albumService.addAdditionalAssetToAlbum(
|
final result = await albumService.addAdditionalAssetToAlbum(
|
||||||
selection.value,
|
assets,
|
||||||
album.id,
|
album.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -103,6 +123,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
"added": result.successfullyAdded.toString(),
|
"added": result.successfullyAdded.toString(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
toastType: ToastType.success,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,8 +132,11 @@ class HomePage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onCreateNewAlbum() async {
|
void onCreateNewAlbum() async {
|
||||||
final result =
|
final Iterable<Asset> assets = remoteOnlySelection();
|
||||||
await albumService.createAlbumWithGeneratedName(selection.value);
|
if (assets.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result = await albumService.createAlbumWithGeneratedName(assets);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SearchResultPageState {
|
class SearchResultPageState {
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool isSuccess;
|
final bool isSuccess;
|
||||||
final bool isError;
|
final bool isError;
|
||||||
final List<AssetResponseDto> searchResult;
|
final List<Asset> searchResult;
|
||||||
|
|
||||||
SearchResultPageState({
|
SearchResultPageState({
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
@@ -20,7 +21,7 @@ class SearchResultPageState {
|
|||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool? isSuccess,
|
bool? isSuccess,
|
||||||
bool? isError,
|
bool? isError,
|
||||||
List<AssetResponseDto>? searchResult,
|
List<Asset>? searchResult,
|
||||||
}) {
|
}) {
|
||||||
return SearchResultPageState(
|
return SearchResultPageState(
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
@@ -44,8 +45,9 @@ class SearchResultPageState {
|
|||||||
isLoading: map['isLoading'] ?? false,
|
isLoading: map['isLoading'] ?? false,
|
||||||
isSuccess: map['isSuccess'] ?? false,
|
isSuccess: map['isSuccess'] ?? false,
|
||||||
isError: map['isError'] ?? false,
|
isError: map['isError'] ?? false,
|
||||||
searchResult: List<AssetResponseDto>.from(
|
searchResult: List<Asset>.from(
|
||||||
map['searchResult']?.map((x) => AssetResponseDto.mapFromJson(x)),
|
map['searchResult']
|
||||||
|
?.map((x) => Asset.remote(AssetResponseDto.fromJson(x))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import 'package:immich_mobile/modules/search/models/search_result_page_state.mod
|
|||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||||
SearchResultPageNotifier(this._searchService)
|
SearchResultPageNotifier(this._searchService)
|
||||||
@@ -30,8 +30,9 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
|||||||
isSuccess: false,
|
isSuccess: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
List<AssetResponseDto>? assets =
|
List<Asset>? assets = (await _searchService.searchAsset(searchTerm))
|
||||||
await _searchService.searchAsset(searchTerm);
|
?.map((e) => Asset.remote(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (assets != null) {
|
if (assets != null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@@ -61,12 +62,11 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
|||||||
var assets = ref.watch(searchResultPageProvider).searchResult;
|
var assets = ref.watch(searchResultPageProvider).searchResult;
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => DateTime.parse(e.createdAt),
|
(e) => e.createdAt,
|
||||||
(a, b) => b.compareTo(a),
|
(a, b) => b.compareTo(a),
|
||||||
);
|
);
|
||||||
return assets.groupListsBy(
|
return assets.groupListsBy(
|
||||||
(element) => DateFormat('y-MM-dd')
|
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
||||||
.format(DateTime.parse(element.createdAt).toLocal()),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
|||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
||||||
|
|||||||
@@ -65,8 +65,7 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
final args = routeData.argsAs<VideoViewerRouteArgs>();
|
final args = routeData.argsAs<VideoViewerRouteArgs>();
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: VideoViewerPage(
|
child: VideoViewerPage(key: args.key, asset: args.asset));
|
||||||
key: args.key, videoUrl: args.videoUrl, asset: args.asset));
|
|
||||||
},
|
},
|
||||||
BackupControllerRoute.name: (routeData) {
|
BackupControllerRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
@@ -258,9 +257,7 @@ class TabControllerRoute extends PageRouteInfo<void> {
|
|||||||
/// [GalleryViewerPage]
|
/// [GalleryViewerPage]
|
||||||
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
|
||||||
GalleryViewerRoute(
|
GalleryViewerRoute(
|
||||||
{Key? key,
|
{Key? key, required List<Asset> assetList, required Asset asset})
|
||||||
required List<AssetResponseDto> assetList,
|
|
||||||
required AssetResponseDto asset})
|
|
||||||
: super(GalleryViewerRoute.name,
|
: super(GalleryViewerRoute.name,
|
||||||
path: '/gallery-viewer-page',
|
path: '/gallery-viewer-page',
|
||||||
args: GalleryViewerRouteArgs(
|
args: GalleryViewerRouteArgs(
|
||||||
@@ -275,9 +272,9 @@ class GalleryViewerRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final List<AssetResponseDto> assetList;
|
final List<Asset> assetList;
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -291,7 +288,7 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
|
|||||||
ImageViewerRoute(
|
ImageViewerRoute(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required String heroTag,
|
required String heroTag,
|
||||||
required AssetResponseDto asset,
|
required Asset asset,
|
||||||
required String authToken,
|
required String authToken,
|
||||||
required void Function() isZoomedFunction,
|
required void Function() isZoomedFunction,
|
||||||
required ValueNotifier<bool> isZoomedListener,
|
required ValueNotifier<bool> isZoomedListener,
|
||||||
@@ -324,7 +321,7 @@ class ImageViewerRouteArgs {
|
|||||||
|
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
final Asset asset;
|
||||||
|
|
||||||
final String authToken;
|
final String authToken;
|
||||||
|
|
||||||
@@ -343,29 +340,24 @@ class ImageViewerRouteArgs {
|
|||||||
/// generated route for
|
/// generated route for
|
||||||
/// [VideoViewerPage]
|
/// [VideoViewerPage]
|
||||||
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||||
VideoViewerRoute(
|
VideoViewerRoute({Key? key, required Asset asset})
|
||||||
{Key? key, required String videoUrl, required AssetResponseDto asset})
|
|
||||||
: super(VideoViewerRoute.name,
|
: super(VideoViewerRoute.name,
|
||||||
path: '/video-viewer-page',
|
path: '/video-viewer-page',
|
||||||
args: VideoViewerRouteArgs(
|
args: VideoViewerRouteArgs(key: key, asset: asset));
|
||||||
key: key, videoUrl: videoUrl, asset: asset));
|
|
||||||
|
|
||||||
static const String name = 'VideoViewerRoute';
|
static const String name = 'VideoViewerRoute';
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoViewerRouteArgs {
|
class VideoViewerRouteArgs {
|
||||||
const VideoViewerRouteArgs(
|
const VideoViewerRouteArgs({this.key, required this.asset});
|
||||||
{this.key, required this.videoUrl, required this.asset});
|
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final String videoUrl;
|
final Asset asset;
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl, asset: $asset}';
|
return 'VideoViewerRouteArgs{key: $key, asset: $asset}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
126
mobile/lib/shared/models/asset.dart
Normal file
126
mobile/lib/shared/models/asset.dart
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
/// Asset (online or local)
|
||||||
|
class Asset {
|
||||||
|
Asset.remote(this.remote) {
|
||||||
|
local = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Asset.local(this.local) {
|
||||||
|
remote = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
late final AssetResponseDto? remote;
|
||||||
|
late final AssetEntity? local;
|
||||||
|
|
||||||
|
bool get isRemote => remote != null;
|
||||||
|
bool get isLocal => local != null;
|
||||||
|
|
||||||
|
String get deviceId =>
|
||||||
|
isRemote ? remote!.deviceId : Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
|
||||||
|
String get deviceAssetId => isRemote ? remote!.deviceAssetId : local!.id;
|
||||||
|
|
||||||
|
String get id => isLocal ? local!.id : remote!.id;
|
||||||
|
|
||||||
|
double? get latitude =>
|
||||||
|
isLocal ? local!.latitude : remote!.exifInfo?.latitude?.toDouble();
|
||||||
|
|
||||||
|
double? get longitude =>
|
||||||
|
isLocal ? local!.longitude : remote!.exifInfo?.longitude?.toDouble();
|
||||||
|
|
||||||
|
DateTime get createdAt =>
|
||||||
|
isLocal ? local!.createDateTime : DateTime.parse(remote!.createdAt);
|
||||||
|
|
||||||
|
bool get isImage => isLocal
|
||||||
|
? local!.type == AssetType.image
|
||||||
|
: remote!.type == AssetTypeEnum.IMAGE;
|
||||||
|
|
||||||
|
String get duration => isRemote
|
||||||
|
? remote!.duration
|
||||||
|
: Duration(seconds: local!.duration).toString();
|
||||||
|
|
||||||
|
/// use only for tests
|
||||||
|
set createdAt(DateTime val) {
|
||||||
|
if (isRemote) {
|
||||||
|
remote!.createdAt = val.toIso8601String();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
if (other is! Asset) return false;
|
||||||
|
return id == other.id && isLocal == other.isLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
if (isLocal) {
|
||||||
|
json["local"] = _assetEntityToJson(local!);
|
||||||
|
} else {
|
||||||
|
json["remote"] = remote!.toJson();
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Asset? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
final l = json["local"];
|
||||||
|
if (l != null) {
|
||||||
|
return Asset.local(_assetEntityFromJson(l));
|
||||||
|
} else {
|
||||||
|
return Asset.remote(AssetResponseDto.fromJson(json["remote"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _assetEntityToJson(AssetEntity a) {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json["id"] = a.id;
|
||||||
|
json["typeInt"] = a.typeInt;
|
||||||
|
json["width"] = a.width;
|
||||||
|
json["height"] = a.height;
|
||||||
|
json["duration"] = a.duration;
|
||||||
|
json["orientation"] = a.orientation;
|
||||||
|
json["isFavorite"] = a.isFavorite;
|
||||||
|
json["title"] = a.title;
|
||||||
|
json["createDateSecond"] = a.createDateSecond;
|
||||||
|
json["modifiedDateSecond"] = a.modifiedDateSecond;
|
||||||
|
json["latitude"] = a.latitude;
|
||||||
|
json["longitude"] = a.longitude;
|
||||||
|
json["mimeType"] = a.mimeType;
|
||||||
|
json["subtype"] = a.subtype;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetEntity? _assetEntityFromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
return AssetEntity(
|
||||||
|
id: json["id"],
|
||||||
|
typeInt: json["typeInt"],
|
||||||
|
width: json["width"],
|
||||||
|
height: json["height"],
|
||||||
|
duration: json["duration"],
|
||||||
|
orientation: json["orientation"],
|
||||||
|
isFavorite: json["isFavorite"],
|
||||||
|
title: json["title"],
|
||||||
|
createDateSecond: json["createDateSecond"],
|
||||||
|
modifiedDateSecond: json["modifiedDateSecond"],
|
||||||
|
latitude: json["latitude"],
|
||||||
|
longitude: json["longitude"],
|
||||||
|
mimeType: json["mimeType"],
|
||||||
|
subtype: json["subtype"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||||
final AssetService _assetService;
|
final AssetService _assetService;
|
||||||
final AssetCacheService _assetCacheService;
|
final AssetCacheService _assetCacheService;
|
||||||
|
|
||||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||||
|
bool _getAllAssetInProgress = false;
|
||||||
|
bool _deleteInProgress = false;
|
||||||
|
|
||||||
AssetNotifier(this._assetService, this._assetCacheService) : super([]);
|
AssetNotifier(this._assetService, this._assetCacheService) : super([]);
|
||||||
|
|
||||||
@@ -21,29 +26,38 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllAsset() async {
|
getAllAsset() async {
|
||||||
final stopwatch = Stopwatch();
|
if (_getAllAssetInProgress || _deleteInProgress) {
|
||||||
|
// guard against multiple calls to this method while it's still working
|
||||||
|
return;
|
||||||
if (await _assetCacheService.isValid() && state.isEmpty) {
|
|
||||||
stopwatch.start();
|
|
||||||
state = await _assetCacheService.get();
|
|
||||||
debugPrint("Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
|
|
||||||
stopwatch.reset();
|
|
||||||
}
|
}
|
||||||
|
final stopwatch = Stopwatch();
|
||||||
|
try {
|
||||||
|
_getAllAssetInProgress = true;
|
||||||
|
|
||||||
|
final bool isCacheValid = await _assetCacheService.isValid();
|
||||||
|
if (isCacheValid && 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(urgent: !isCacheValid);
|
||||||
|
debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
stopwatch.reset();
|
||||||
|
|
||||||
|
state = allAssets;
|
||||||
|
} finally {
|
||||||
|
_getAllAssetInProgress = false;
|
||||||
|
}
|
||||||
|
debugPrint("[getAllAsset] setting new asset state");
|
||||||
|
|
||||||
stopwatch.start();
|
stopwatch.start();
|
||||||
var allAssets = await _assetService.getAllAsset();
|
_cacheState();
|
||||||
debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
|
debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
stopwatch.reset();
|
stopwatch.reset();
|
||||||
|
|
||||||
if (allAssets != null) {
|
|
||||||
state = allAssets;
|
|
||||||
|
|
||||||
stopwatch.start();
|
|
||||||
_cacheState();
|
|
||||||
debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
|
||||||
stopwatch.reset();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAllAsset() {
|
clearAllAsset() {
|
||||||
@@ -52,80 +66,113 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onNewAssetUploaded(AssetResponseDto newAsset) {
|
onNewAssetUploaded(AssetResponseDto newAsset) {
|
||||||
state = [...state, newAsset];
|
final int i = state.indexWhere(
|
||||||
|
(a) =>
|
||||||
|
a.isRemote ||
|
||||||
|
(a.id == newAsset.deviceAssetId && a.deviceId == newAsset.deviceId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (i == -1 || state[i].deviceAssetId != newAsset.deviceAssetId) {
|
||||||
|
state = [...state, Asset.remote(newAsset)];
|
||||||
|
} else {
|
||||||
|
// order is important to keep all local-only assets at the beginning!
|
||||||
|
state = [
|
||||||
|
...state.slice(0, i),
|
||||||
|
...state.slice(i + 1),
|
||||||
|
Asset.remote(newAsset),
|
||||||
|
];
|
||||||
|
// TODO here is a place to unify local/remote assets by replacing the
|
||||||
|
// local-only asset in the state with a local&remote asset
|
||||||
|
}
|
||||||
_cacheState();
|
_cacheState();
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAssets(Set<AssetResponseDto> deleteAssets) async {
|
deleteAssets(Set<Asset> deleteAssets) async {
|
||||||
|
_deleteInProgress = true;
|
||||||
|
try {
|
||||||
|
final localDeleted = await _deleteLocalAssets(deleteAssets);
|
||||||
|
final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
|
||||||
|
final Set<String> deleted = HashSet();
|
||||||
|
deleted.addAll(localDeleted);
|
||||||
|
deleted.addAll(remoteDeleted);
|
||||||
|
if (deleted.isNotEmpty) {
|
||||||
|
state = state.where((a) => !deleted.contains(a.id)).toList();
|
||||||
|
_cacheState();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_deleteInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _deleteLocalAssets(Set<Asset> assetsToDelete) async {
|
||||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||||
var deviceId = deviceInfo["deviceId"];
|
var deviceId = deviceInfo["deviceId"];
|
||||||
var deleteIdList = <String>[];
|
final List<String> local = [];
|
||||||
// Delete asset from device
|
// Delete asset from device
|
||||||
for (var asset in deleteAssets) {
|
for (final Asset asset in assetsToDelete) {
|
||||||
// Delete asset on device if present
|
if (asset.isLocal) {
|
||||||
if (asset.deviceId == deviceId) {
|
local.add(asset.id);
|
||||||
|
} else if (asset.deviceId == deviceId) {
|
||||||
|
// Delete asset on device if it is still present
|
||||||
var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
||||||
|
|
||||||
if (localAsset != null) {
|
if (localAsset != null) {
|
||||||
deleteIdList.add(localAsset.id);
|
local.add(localAsset.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (local.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
await PhotoManager.editor.deleteWithIds(deleteIdList);
|
return await PhotoManager.editor.deleteWithIds(local);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Delete asset from device failed: $e");
|
debugPrint("Delete asset from device failed: $e");
|
||||||
}
|
|
||||||
|
|
||||||
// Delete asset on server
|
|
||||||
List<DeleteAssetResponseDto>? deleteAssetResult =
|
|
||||||
await _assetService.deleteAssets(deleteAssets);
|
|
||||||
|
|
||||||
if (deleteAssetResult == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var asset in deleteAssetResult) {
|
|
||||||
if (asset.status == DeleteAssetStatus.SUCCESS) {
|
|
||||||
state =
|
|
||||||
state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
_cacheState();
|
Future<Iterable<String>> _deleteRemoteAssets(
|
||||||
|
Set<Asset> assetsToDelete,
|
||||||
|
) async {
|
||||||
|
final Iterable<AssetResponseDto> remote =
|
||||||
|
assetsToDelete.where((e) => e.isRemote).map((e) => e.remote!);
|
||||||
|
final List<DeleteAssetResponseDto> deleteAssetResult =
|
||||||
|
await _assetService.deleteAssets(remote) ?? [];
|
||||||
|
return deleteAssetResult
|
||||||
|
.where((a) => a.status == DeleteAssetStatus.SUCCESS)
|
||||||
|
.map((a) => a.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetProvider =
|
final assetProvider = StateNotifierProvider<AssetNotifier, List<Asset>>((ref) {
|
||||||
StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
|
|
||||||
return AssetNotifier(
|
return AssetNotifier(
|
||||||
ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
|
ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(assetProvider);
|
final assets = ref.watch(assetProvider).toList();
|
||||||
|
// `toList()` ist needed to make a copy as to NOT sort the original list/state
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => DateTime.parse(e.createdAt),
|
(e) => e.createdAt,
|
||||||
(a, b) => b.compareTo(a),
|
(a, b) => b.compareTo(a),
|
||||||
);
|
);
|
||||||
return assets.groupListsBy(
|
return assets.groupListsBy(
|
||||||
(element) => DateFormat('y-MM-dd')
|
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
||||||
.format(DateTime.parse(element.createdAt).toLocal()),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(assetProvider);
|
// TODO: remove `where` once temporary workaround is no longer needed (to only
|
||||||
|
// allow remote assets to be added to album). Keep `toList()` as to NOT sort
|
||||||
|
// the original list/state
|
||||||
|
final assets = ref.watch(assetProvider).where((e) => e.isRemote).toList();
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => DateTime.parse(e.createdAt),
|
(e) => e.createdAt,
|
||||||
(a, b) => b.compareTo(a),
|
(a, b) => b.compareTo(a),
|
||||||
);
|
);
|
||||||
|
|
||||||
return assets.groupListsBy(
|
return assets.groupListsBy(
|
||||||
(element) => DateFormat('MMMM, y')
|
(element) => DateFormat('MMMM, y').format(element.createdAt.toLocal()),
|
||||||
.format(DateTime.parse(element.createdAt).toLocal()),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.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:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'api.service.dart';
|
import 'api.service.dart';
|
||||||
|
|
||||||
final shareServiceProvider =
|
final shareServiceProvider =
|
||||||
@@ -17,26 +17,28 @@ class ShareService {
|
|||||||
|
|
||||||
ShareService(this._apiService);
|
ShareService(this._apiService);
|
||||||
|
|
||||||
Future<void> shareAsset(AssetResponseDto asset) async {
|
Future<void> shareAsset(Asset asset) async {
|
||||||
await shareAssets([asset]);
|
await shareAssets([asset]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> shareAssets(List<AssetResponseDto> assets) async {
|
Future<void> shareAssets(List<Asset> assets) async {
|
||||||
final downloadedFilePaths = assets.map((asset) async {
|
final downloadedFilePaths = assets.map((asset) async {
|
||||||
final res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
if (asset.isRemote) {
|
||||||
asset.deviceAssetId,
|
final tempDir = await getTemporaryDirectory();
|
||||||
asset.deviceId,
|
final fileName = basename(asset.remote!.originalPath);
|
||||||
isThumb: false,
|
final tempFile = await File('${tempDir.path}/$fileName').create();
|
||||||
isWeb: false,
|
final res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||||
);
|
asset.remote!.deviceAssetId,
|
||||||
|
asset.remote!.deviceId,
|
||||||
final fileName = p.basename(asset.originalPath);
|
isThumb: false,
|
||||||
|
isWeb: false,
|
||||||
final tempDir = await getTemporaryDirectory();
|
);
|
||||||
final tempFile = await File('${tempDir.path}/$fileName').create();
|
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||||
tempFile.writeAsBytesSync(res.bodyBytes);
|
return tempFile.path;
|
||||||
|
} else {
|
||||||
return tempFile.path;
|
File? f = await asset.local!.file;
|
||||||
|
return f!.path;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Share.shareFiles(
|
Share.shareFiles(
|
||||||
|
|||||||
96
mobile/lib/shared/ui/immich_image.dart
Normal file
96
mobile/lib/shared/ui/immich_image.dart
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
/// Renders an Asset using local data if available, else remote data
|
||||||
|
class ImmichImage extends StatelessWidget {
|
||||||
|
const ImmichImage(
|
||||||
|
this.asset, {
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
this.useGrayBoxPlaceholder = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final Asset asset;
|
||||||
|
final bool useGrayBoxPlaceholder;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (asset.isLocal) {
|
||||||
|
return Image(
|
||||||
|
image: AssetEntityImageProvider(
|
||||||
|
asset.local!,
|
||||||
|
isOriginal: false,
|
||||||
|
thumbnailSize: const ThumbnailSize.square(250), // like server thumbs
|
||||||
|
),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return (useGrayBoxPlaceholder
|
||||||
|
? const SizedBox.square(
|
||||||
|
dimension: 250,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(color: Colors.grey),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Transform.scale(
|
||||||
|
scale: 0.2,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
debugPrint("Error getting thumb for assetId=${asset.id}: $error");
|
||||||
|
return Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final String token = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
final String thumbnailRequestUrl = getThumbnailUrl(asset.remote!);
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: thumbnailRequestUrl,
|
||||||
|
httpHeaders: {"Authorization": "Bearer $token"},
|
||||||
|
cacheKey: 'thumbnail-image-${asset.id}',
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
// keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
|
||||||
|
// maxHeightDiskCache = null allows to simply store the webp thumbnail
|
||||||
|
// from the server and use it for all rendered thumbnail sizes
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: const Duration(milliseconds: 250),
|
||||||
|
progressIndicatorBuilder: (context, url, downloadProgress) {
|
||||||
|
if (useGrayBoxPlaceholder) {
|
||||||
|
return const DecoratedBox(
|
||||||
|
decoration: BoxDecoration(color: Colors.grey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Transform.scale(
|
||||||
|
scale: 0.2,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: downloadProgress.progress,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorWidget: (context, url, error) {
|
||||||
|
debugPrint("Error getting thumbnail $url = $error");
|
||||||
|
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
|
||||||
|
return Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ doc/SmartInfoResponseDto.md
|
|||||||
doc/ThumbnailFormat.md
|
doc/ThumbnailFormat.md
|
||||||
doc/TimeGroupEnum.md
|
doc/TimeGroupEnum.md
|
||||||
doc/UpdateAlbumDto.md
|
doc/UpdateAlbumDto.md
|
||||||
|
doc/UpdateAssetDto.md
|
||||||
doc/UpdateDeviceInfoDto.md
|
doc/UpdateDeviceInfoDto.md
|
||||||
doc/UpdateUserDto.md
|
doc/UpdateUserDto.md
|
||||||
doc/UsageByUserDto.md
|
doc/UsageByUserDto.md
|
||||||
@@ -132,6 +133,7 @@ lib/model/smart_info_response_dto.dart
|
|||||||
lib/model/thumbnail_format.dart
|
lib/model/thumbnail_format.dart
|
||||||
lib/model/time_group_enum.dart
|
lib/model/time_group_enum.dart
|
||||||
lib/model/update_album_dto.dart
|
lib/model/update_album_dto.dart
|
||||||
|
lib/model/update_asset_dto.dart
|
||||||
lib/model/update_device_info_dto.dart
|
lib/model/update_device_info_dto.dart
|
||||||
lib/model/update_user_dto.dart
|
lib/model/update_user_dto.dart
|
||||||
lib/model/usage_by_user_dto.dart
|
lib/model/usage_by_user_dto.dart
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ Class | Method | HTTP request | Description
|
|||||||
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||||
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
|
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
|
||||||
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file |
|
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file |
|
||||||
|
*AssetApi* | [**updateAssetById**](doc//AssetApi.md#updateassetbyid) | **PUT** /asset/assetById/{assetId} |
|
||||||
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
|
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
|
||||||
*AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |
|
*AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |
|
||||||
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
||||||
@@ -108,11 +109,13 @@ Class | Method | HTTP request | Description
|
|||||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||||
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image |
|
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image |
|
||||||
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user |
|
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user |
|
||||||
|
*UserApi* | [**deleteUser**](doc//UserApi.md#deleteuser) | **DELETE** /user/{userId} |
|
||||||
*UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user |
|
*UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user |
|
||||||
*UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me |
|
*UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me |
|
||||||
*UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} |
|
*UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} |
|
||||||
*UserApi* | [**getUserById**](doc//UserApi.md#getuserbyid) | **GET** /user/info/{userId} |
|
*UserApi* | [**getUserById**](doc//UserApi.md#getuserbyid) | **GET** /user/info/{userId} |
|
||||||
*UserApi* | [**getUserCount**](doc//UserApi.md#getusercount) | **GET** /user/count |
|
*UserApi* | [**getUserCount**](doc//UserApi.md#getusercount) | **GET** /user/count |
|
||||||
|
*UserApi* | [**restoreUser**](doc//UserApi.md#restoreuser) | **POST** /user/{userId}/restore |
|
||||||
*UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user |
|
*UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user |
|
||||||
|
|
||||||
|
|
||||||
@@ -168,6 +171,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [ThumbnailFormat](doc//ThumbnailFormat.md)
|
- [ThumbnailFormat](doc//ThumbnailFormat.md)
|
||||||
- [TimeGroupEnum](doc//TimeGroupEnum.md)
|
- [TimeGroupEnum](doc//TimeGroupEnum.md)
|
||||||
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
||||||
|
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||||
- [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
|
- [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
|
||||||
- [UpdateUserDto](doc//UpdateUserDto.md)
|
- [UpdateUserDto](doc//UpdateUserDto.md)
|
||||||
- [UsageByUserDto](doc//UsageByUserDto.md)
|
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Method | HTTP request | Description
|
|||||||
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||||
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
|
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
|
||||||
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file |
|
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file |
|
||||||
|
[**updateAssetById**](AssetApi.md#updateassetbyid) | **PUT** /asset/assetById/{assetId} |
|
||||||
[**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |
|
[**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |
|
||||||
|
|
||||||
|
|
||||||
@@ -784,6 +785,57 @@ Name | Type | Description | Notes
|
|||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **updateAssetById**
|
||||||
|
> AssetResponseDto updateAssetById(assetId, updateAssetDto)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Update an asset
|
||||||
|
|
||||||
|
### 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 assetId = assetId_example; // String |
|
||||||
|
final updateAssetDto = UpdateAssetDto(); // UpdateAssetDto |
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.updateAssetById(assetId, updateAssetDto);
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AssetApi->updateAssetById: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**assetId** | **String**| |
|
||||||
|
**updateAssetDto** | [**UpdateAssetDto**](UpdateAssetDto.md)| |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**AssetResponseDto**](AssetResponseDto.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)
|
||||||
|
|
||||||
# **uploadFile**
|
# **uploadFile**
|
||||||
> AssetFileUploadResponseDto uploadFile(assetData)
|
> AssetFileUploadResponseDto uploadFile(assetData)
|
||||||
|
|
||||||
|
|||||||
15
mobile/openapi/doc/UpdateAssetDto.md
Normal file
15
mobile/openapi/doc/UpdateAssetDto.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# openapi.model.UpdateAssetDto
|
||||||
|
|
||||||
|
## Load the model package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**isFavorite** | **bool** | |
|
||||||
|
|
||||||
|
[[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,11 +11,13 @@ Method | HTTP request | Description
|
|||||||
------------- | ------------- | -------------
|
------------- | ------------- | -------------
|
||||||
[**createProfileImage**](UserApi.md#createprofileimage) | **POST** /user/profile-image |
|
[**createProfileImage**](UserApi.md#createprofileimage) | **POST** /user/profile-image |
|
||||||
[**createUser**](UserApi.md#createuser) | **POST** /user |
|
[**createUser**](UserApi.md#createuser) | **POST** /user |
|
||||||
|
[**deleteUser**](UserApi.md#deleteuser) | **DELETE** /user/{userId} |
|
||||||
[**getAllUsers**](UserApi.md#getallusers) | **GET** /user |
|
[**getAllUsers**](UserApi.md#getallusers) | **GET** /user |
|
||||||
[**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me |
|
[**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me |
|
||||||
[**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} |
|
[**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} |
|
||||||
[**getUserById**](UserApi.md#getuserbyid) | **GET** /user/info/{userId} |
|
[**getUserById**](UserApi.md#getuserbyid) | **GET** /user/info/{userId} |
|
||||||
[**getUserCount**](UserApi.md#getusercount) | **GET** /user/count |
|
[**getUserCount**](UserApi.md#getusercount) | **GET** /user/count |
|
||||||
|
[**restoreUser**](UserApi.md#restoreuser) | **POST** /user/{userId}/restore |
|
||||||
[**updateUser**](UserApi.md#updateuser) | **PUT** /user |
|
[**updateUser**](UserApi.md#updateuser) | **PUT** /user |
|
||||||
|
|
||||||
|
|
||||||
@@ -113,6 +115,53 @@ Name | Type | Description | Notes
|
|||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **deleteUser**
|
||||||
|
> UserResponseDto deleteUser(userId)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 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 = UserApi();
|
||||||
|
final userId = userId_example; // String |
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.deleteUser(userId);
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling UserApi->deleteUser: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**userId** | **String**| |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**UserResponseDto**](UserResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **getAllUsers**
|
# **getAllUsers**
|
||||||
> List<UserResponseDto> getAllUsers(isAll)
|
> List<UserResponseDto> getAllUsers(isAll)
|
||||||
|
|
||||||
@@ -322,6 +371,53 @@ 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)
|
[[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)
|
||||||
|
|
||||||
|
# **restoreUser**
|
||||||
|
> UserResponseDto restoreUser(userId)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 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 = UserApi();
|
||||||
|
final userId = userId_example; // String |
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.restoreUser(userId);
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling UserApi->restoreUser: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**userId** | **String**| |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**UserResponseDto**](UserResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **updateUser**
|
# **updateUser**
|
||||||
> UserResponseDto updateUser(updateUserDto)
|
> UserResponseDto updateUser(updateUserDto)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Name | Type | Description | Notes
|
|||||||
**profileImagePath** | **String** | |
|
**profileImagePath** | **String** | |
|
||||||
**shouldChangePassword** | **bool** | |
|
**shouldChangePassword** | **bool** | |
|
||||||
**isAdmin** | **bool** | |
|
**isAdmin** | **bool** | |
|
||||||
|
**deletedAt** | [**DateTime**](DateTime.md) | |
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ part 'model/smart_info_response_dto.dart';
|
|||||||
part 'model/thumbnail_format.dart';
|
part 'model/thumbnail_format.dart';
|
||||||
part 'model/time_group_enum.dart';
|
part 'model/time_group_enum.dart';
|
||||||
part 'model/update_album_dto.dart';
|
part 'model/update_album_dto.dart';
|
||||||
|
part 'model/update_asset_dto.dart';
|
||||||
part 'model/update_device_info_dto.dart';
|
part 'model/update_device_info_dto.dart';
|
||||||
part 'model/update_user_dto.dart';
|
part 'model/update_user_dto.dart';
|
||||||
part 'model/usage_by_user_dto.dart';
|
part 'model/usage_by_user_dto.dart';
|
||||||
|
|||||||
@@ -858,6 +858,67 @@ class AssetApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Update an asset
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] assetId (required):
|
||||||
|
///
|
||||||
|
/// * [UpdateAssetDto] updateAssetDto (required):
|
||||||
|
Future<Response> updateAssetByIdWithHttpInfo(String assetId, UpdateAssetDto updateAssetDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/asset/assetById/{assetId}'
|
||||||
|
.replaceAll('{assetId}', assetId);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = updateAssetDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'PUT',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Update an asset
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] assetId (required):
|
||||||
|
///
|
||||||
|
/// * [UpdateAssetDto] updateAssetDto (required):
|
||||||
|
Future<AssetResponseDto?> updateAssetById(String assetId, UpdateAssetDto updateAssetDto,) async {
|
||||||
|
final response = await updateAssetByIdWithHttpInfo(assetId, updateAssetDto,);
|
||||||
|
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), 'AssetResponseDto',) as AssetResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /asset/upload' operation and returns the [Response].
|
/// Performs an HTTP 'POST /asset/upload' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -120,6 +120,54 @@ class UserApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'DELETE /user/{userId}' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] userId (required):
|
||||||
|
Future<Response> deleteUserWithHttpInfo(String userId,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/user/{userId}'
|
||||||
|
.replaceAll('{userId}', userId);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] userId (required):
|
||||||
|
Future<UserResponseDto?> deleteUser(String userId,) async {
|
||||||
|
final response = await deleteUserWithHttpInfo(userId,);
|
||||||
|
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), 'UserResponseDto',) as UserResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /user' operation and returns the [Response].
|
/// Performs an HTTP 'GET /user' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
@@ -350,6 +398,54 @@ class UserApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /user/{userId}/restore' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] userId (required):
|
||||||
|
Future<Response> restoreUserWithHttpInfo(String userId,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/user/{userId}/restore'
|
||||||
|
.replaceAll('{userId}', userId);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] userId (required):
|
||||||
|
Future<UserResponseDto?> restoreUser(String userId,) async {
|
||||||
|
final response = await restoreUserWithHttpInfo(userId,);
|
||||||
|
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), 'UserResponseDto',) as UserResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'PUT /user' operation and returns the [Response].
|
/// Performs an HTTP 'PUT /user' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -292,6 +292,8 @@ class ApiClient {
|
|||||||
return TimeGroupEnumTypeTransformer().decode(value);
|
return TimeGroupEnumTypeTransformer().decode(value);
|
||||||
case 'UpdateAlbumDto':
|
case 'UpdateAlbumDto':
|
||||||
return UpdateAlbumDto.fromJson(value);
|
return UpdateAlbumDto.fromJson(value);
|
||||||
|
case 'UpdateAssetDto':
|
||||||
|
return UpdateAssetDto.fromJson(value);
|
||||||
case 'UpdateDeviceInfoDto':
|
case 'UpdateDeviceInfoDto':
|
||||||
return UpdateDeviceInfoDto.fromJson(value);
|
return UpdateDeviceInfoDto.fromJson(value);
|
||||||
case 'UpdateUserDto':
|
case 'UpdateUserDto':
|
||||||
|
|||||||
111
mobile/openapi/lib/model/update_asset_dto.dart
Normal file
111
mobile/openapi/lib/model/update_asset_dto.dart
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
//
|
||||||
|
// 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 UpdateAssetDto {
|
||||||
|
/// Returns a new [UpdateAssetDto] instance.
|
||||||
|
UpdateAssetDto({
|
||||||
|
required this.isFavorite,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool isFavorite;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
|
||||||
|
other.isFavorite == isFavorite;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(isFavorite.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'UpdateAssetDto[isFavorite=$isFavorite]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_json[r'isFavorite'] = isFavorite;
|
||||||
|
return _json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [UpdateAssetDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static UpdateAssetDto? 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 "UpdateAssetDto[$key]" is missing from JSON.');
|
||||||
|
assert(json[key] != null, 'Required key "UpdateAssetDto[$key]" has a null value in JSON.');
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
return UpdateAssetDto(
|
||||||
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<UpdateAssetDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <UpdateAssetDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = UpdateAssetDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, UpdateAssetDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, UpdateAssetDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = UpdateAssetDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of UpdateAssetDto-objects as value to a dart map
|
||||||
|
static Map<String, List<UpdateAssetDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<UpdateAssetDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = UpdateAssetDto.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>{
|
||||||
|
'isFavorite',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ class UserResponseDto {
|
|||||||
required this.profileImagePath,
|
required this.profileImagePath,
|
||||||
required this.shouldChangePassword,
|
required this.shouldChangePassword,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
|
required this.deletedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
@@ -39,6 +40,8 @@ class UserResponseDto {
|
|||||||
|
|
||||||
bool isAdmin;
|
bool isAdmin;
|
||||||
|
|
||||||
|
DateTime? deletedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
@@ -48,7 +51,8 @@ class UserResponseDto {
|
|||||||
other.createdAt == createdAt &&
|
other.createdAt == createdAt &&
|
||||||
other.profileImagePath == profileImagePath &&
|
other.profileImagePath == profileImagePath &&
|
||||||
other.shouldChangePassword == shouldChangePassword &&
|
other.shouldChangePassword == shouldChangePassword &&
|
||||||
other.isAdmin == isAdmin;
|
other.isAdmin == isAdmin &&
|
||||||
|
other.deletedAt == deletedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@@ -60,10 +64,11 @@ class UserResponseDto {
|
|||||||
(createdAt.hashCode) +
|
(createdAt.hashCode) +
|
||||||
(profileImagePath.hashCode) +
|
(profileImagePath.hashCode) +
|
||||||
(shouldChangePassword.hashCode) +
|
(shouldChangePassword.hashCode) +
|
||||||
(isAdmin.hashCode);
|
(isAdmin.hashCode) +
|
||||||
|
(deletedAt == null ? 0 : deletedAt!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin]';
|
String toString() => 'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, deletedAt=$deletedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final _json = <String, dynamic>{};
|
final _json = <String, dynamic>{};
|
||||||
@@ -75,6 +80,11 @@ class UserResponseDto {
|
|||||||
_json[r'profileImagePath'] = profileImagePath;
|
_json[r'profileImagePath'] = profileImagePath;
|
||||||
_json[r'shouldChangePassword'] = shouldChangePassword;
|
_json[r'shouldChangePassword'] = shouldChangePassword;
|
||||||
_json[r'isAdmin'] = isAdmin;
|
_json[r'isAdmin'] = isAdmin;
|
||||||
|
if (deletedAt != null) {
|
||||||
|
_json[r'deletedAt'] = deletedAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
_json[r'deletedAt'] = null;
|
||||||
|
}
|
||||||
return _json;
|
return _json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +115,7 @@ class UserResponseDto {
|
|||||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
||||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
||||||
|
deletedAt: mapDateTime(json, r'deletedAt', ''),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -162,6 +173,7 @@ class UserResponseDto {
|
|||||||
'profileImagePath',
|
'profileImagePath',
|
||||||
'shouldChangePassword',
|
'shouldChangePassword',
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
|
'deletedAt',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
27
mobile/openapi/test/update_asset_dto_test.dart
Normal file
27
mobile/openapi/test/update_asset_dto_test.dart
Normal file
@@ -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 UpdateAssetDto
|
||||||
|
void main() {
|
||||||
|
// final instance = UpdateAssetDto();
|
||||||
|
|
||||||
|
group('test UpdateAssetDto', () {
|
||||||
|
// bool isFavorite
|
||||||
|
test('to test the property `isFavorite`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.34.0+53
|
version: 1.35.0+54
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final List<AssetResponseDto> testAssets = [];
|
final List<Asset> testAssets = [];
|
||||||
|
|
||||||
for (int i = 0; i < 150; i++) {
|
for (int i = 0; i < 150; i++) {
|
||||||
int month = i ~/ 31;
|
int month = i ~/ 31;
|
||||||
@@ -11,39 +12,43 @@ void main() {
|
|||||||
|
|
||||||
DateTime date = DateTime(2022, month, day);
|
DateTime date = DateTime(2022, month, day);
|
||||||
|
|
||||||
testAssets.add(AssetResponseDto(
|
testAssets.add(
|
||||||
type: AssetTypeEnum.IMAGE,
|
Asset.remote(
|
||||||
id: '$i',
|
AssetResponseDto(
|
||||||
deviceAssetId: '',
|
type: AssetTypeEnum.IMAGE,
|
||||||
ownerId: '',
|
id: '$i',
|
||||||
deviceId: '',
|
deviceAssetId: '',
|
||||||
originalPath: '',
|
ownerId: '',
|
||||||
resizePath: '',
|
deviceId: '',
|
||||||
createdAt: date.toIso8601String(),
|
originalPath: '',
|
||||||
modifiedAt: date.toIso8601String(),
|
resizePath: '',
|
||||||
isFavorite: false,
|
createdAt: date.toIso8601String(),
|
||||||
mimeType: 'image/jpeg',
|
modifiedAt: date.toIso8601String(),
|
||||||
duration: '',
|
isFavorite: false,
|
||||||
webpPath: '',
|
mimeType: 'image/jpeg',
|
||||||
encodedVideoPath: '',
|
duration: '',
|
||||||
));
|
webpPath: '',
|
||||||
|
encodedVideoPath: '',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, List<AssetResponseDto>> groups = {
|
final Map<String, List<Asset>> groups = {
|
||||||
'2022-01-05': testAssets.sublist(0, 5).map((e) {
|
'2022-01-05': testAssets.sublist(0, 5).map((e) {
|
||||||
e.createdAt = DateTime(2022, 1, 5).toIso8601String();
|
e.createdAt = DateTime(2022, 1, 5);
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'2022-01-10': testAssets.sublist(5, 10).map((e) {
|
'2022-01-10': testAssets.sublist(5, 10).map((e) {
|
||||||
e.createdAt = DateTime(2022, 1, 10).toIso8601String();
|
e.createdAt = DateTime(2022, 1, 10);
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'2022-02-17': testAssets.sublist(10, 15).map((e) {
|
'2022-02-17': testAssets.sublist(10, 15).map((e) {
|
||||||
e.createdAt = DateTime(2022, 2, 17).toIso8601String();
|
e.createdAt = DateTime(2022, 2, 17);
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'2022-10-15': testAssets.sublist(15, 30).map((e) {
|
'2022-10-15': testAssets.sublist(15, 30).map((e) {
|
||||||
e.createdAt = DateTime(2022, 10, 15).toIso8601String();
|
e.createdAt = DateTime(2022, 10, 15);
|
||||||
return e;
|
return e;
|
||||||
}).toList()
|
}).toList()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { BadRequestException, NotFoundException, ForbiddenException } from '@nes
|
|||||||
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 { IAssetRepository } from '../asset/asset-repository';
|
import { IAssetRepository } from '../asset/asset-repository';
|
||||||
import {AddAssetsResponseDto} from "./response-dto/add-assets-response.dto";
|
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||||
import {IAlbumRepository} from "./album-repository";
|
import { IAlbumRepository } from './album-repository';
|
||||||
|
|
||||||
describe('Album service', () => {
|
describe('Album service', () => {
|
||||||
let sut: AlbumService;
|
let sut: AlbumService;
|
||||||
@@ -125,6 +125,7 @@ describe('Album service', () => {
|
|||||||
|
|
||||||
assetRepositoryMock = {
|
assetRepositoryMock = {
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
getAllByUserId: jest.fn(),
|
getAllByUserId: jest.fn(),
|
||||||
getAllByDeviceId: jest.fn(),
|
getAllByDeviceId: jest.fn(),
|
||||||
getAssetCountByTimeBucket: jest.fn(),
|
getAssetCountByTimeBucket: jest.fn(),
|
||||||
@@ -333,7 +334,7 @@ describe('Album service', () => {
|
|||||||
|
|
||||||
const albumResponse: AddAssetsResponseDto = {
|
const albumResponse: AddAssetsResponseDto = {
|
||||||
alreadyInAlbum: [],
|
alreadyInAlbum: [],
|
||||||
successfullyAdded: 1
|
successfullyAdded: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const albumId = albumEntity.id;
|
const albumId = albumEntity.id;
|
||||||
@@ -341,13 +342,13 @@ describe('Album service', () => {
|
|||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
|
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
|
||||||
|
|
||||||
const result = await sut.addAssetsToAlbum(
|
const result = (await sut.addAssetsToAlbum(
|
||||||
authUser,
|
authUser,
|
||||||
{
|
{
|
||||||
assetIds: ['1'],
|
assetIds: ['1'],
|
||||||
},
|
},
|
||||||
albumId,
|
albumId,
|
||||||
) as AddAssetsResponseDto;
|
)) as AddAssetsResponseDto;
|
||||||
|
|
||||||
// TODO: stub and expect album rendered
|
// TODO: stub and expect album rendered
|
||||||
expect(result.album?.id).toEqual(albumId);
|
expect(result.album?.id).toEqual(albumId);
|
||||||
@@ -358,7 +359,7 @@ describe('Album service', () => {
|
|||||||
|
|
||||||
const albumResponse: AddAssetsResponseDto = {
|
const albumResponse: AddAssetsResponseDto = {
|
||||||
alreadyInAlbum: [],
|
alreadyInAlbum: [],
|
||||||
successfullyAdded: 1
|
successfullyAdded: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const albumId = albumEntity.id;
|
const albumId = albumEntity.id;
|
||||||
@@ -366,13 +367,13 @@ describe('Album service', () => {
|
|||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||||
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
|
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
|
||||||
|
|
||||||
const result = await sut.addAssetsToAlbum(
|
const result = (await sut.addAssetsToAlbum(
|
||||||
authUser,
|
authUser,
|
||||||
{
|
{
|
||||||
assetIds: ['1'],
|
assetIds: ['1'],
|
||||||
},
|
},
|
||||||
albumId,
|
albumId,
|
||||||
) as AddAssetsResponseDto;
|
)) as AddAssetsResponseDto;
|
||||||
|
|
||||||
// TODO: stub and expect album rendered
|
// TODO: stub and expect album rendered
|
||||||
expect(result.album?.id).toEqual(albumId);
|
expect(result.album?.id).toEqual(albumId);
|
||||||
@@ -383,7 +384,7 @@ describe('Album service', () => {
|
|||||||
|
|
||||||
const albumResponse: AddAssetsResponseDto = {
|
const albumResponse: AddAssetsResponseDto = {
|
||||||
alreadyInAlbum: [],
|
alreadyInAlbum: [],
|
||||||
successfullyAdded: 1
|
successfullyAdded: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const albumId = albumEntity.id;
|
const albumId = albumEntity.id;
|
||||||
@@ -447,7 +448,7 @@ describe('Album service', () => {
|
|||||||
|
|
||||||
const albumResponse: AddAssetsResponseDto = {
|
const albumResponse: AddAssetsResponseDto = {
|
||||||
alreadyInAlbum: [],
|
alreadyInAlbum: [],
|
||||||
successfullyAdded: 1
|
successfullyAdded: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const albumId = albumEntity.id;
|
const albumId = albumEntity.id;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
|
|||||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||||
import { In } from 'typeorm/find-options/operator/In';
|
import { In } from 'typeorm/find-options/operator/In';
|
||||||
|
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
create(
|
create(
|
||||||
@@ -22,6 +23,7 @@ export interface IAssetRepository {
|
|||||||
mimeType: string,
|
mimeType: string,
|
||||||
checksum?: Buffer,
|
checksum?: Buffer,
|
||||||
): Promise<AssetEntity>;
|
): Promise<AssetEntity>;
|
||||||
|
update(asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
|
||||||
getAllByUserId(userId: string): Promise<AssetEntity[]>;
|
getAllByUserId(userId: string): Promise<AssetEntity[]>;
|
||||||
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
||||||
getById(assetId: string): Promise<AssetEntity>;
|
getById(assetId: string): Promise<AssetEntity>;
|
||||||
@@ -252,6 +254,15 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
return createdAsset;
|
return createdAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update asset
|
||||||
|
*/
|
||||||
|
async update(asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity> {
|
||||||
|
asset.isFavorite = dto.isFavorite ?? asset.isFavorite;
|
||||||
|
|
||||||
|
return await this.assetRepository.save(asset);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get assets by device's Id on the database
|
* Get assets by device's Id on the database
|
||||||
* @param userId
|
* @param userId
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
BadRequestException,
|
BadRequestException,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
Header,
|
Header,
|
||||||
|
Put,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
@@ -50,6 +51,7 @@ import { QueryFailedError } from 'typeorm';
|
|||||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||||
|
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||||
|
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -222,6 +224,18 @@ export class AssetController {
|
|||||||
return await this.assetService.getAssetById(authUser, assetId);
|
return await this.assetService.getAssetById(authUser, assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an asset
|
||||||
|
*/
|
||||||
|
@Put('/assetById/:assetId')
|
||||||
|
async updateAssetById(
|
||||||
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
|
@Param('assetId') assetId: string,
|
||||||
|
@Body() dto: UpdateAssetDto,
|
||||||
|
): Promise<AssetResponseDto> {
|
||||||
|
return await this.assetService.updateAssetById(authUser, assetId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Delete('/')
|
@Delete('/')
|
||||||
async deleteAsset(
|
async deleteAsset(
|
||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ describe('AssetService', () => {
|
|||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
assetRepositoryMock = {
|
assetRepositoryMock = {
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
getAllByUserId: jest.fn(),
|
getAllByUserId: jest.fn(),
|
||||||
getAllByDeviceId: jest.fn(),
|
getAllByDeviceId: jest.fn(),
|
||||||
getAssetCountByTimeBucket: jest.fn(),
|
getAssetCountByTimeBucket: jest.fn(),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
@@ -39,6 +40,7 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
|
|||||||
import { timeUtils } from '@app/common/utils';
|
import { timeUtils } from '@app/common/utils';
|
||||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||||
|
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
@@ -123,6 +125,21 @@ export class AssetService {
|
|||||||
return mapAsset(asset);
|
return mapAsset(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateAssetById(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||||
|
const asset = await this._assetRepository.getById(assetId);
|
||||||
|
if (!asset) {
|
||||||
|
throw new BadRequestException('Asset not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authUser.id !== asset.userId) {
|
||||||
|
throw new ForbiddenException('Not the owner');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAsset = await this._assetRepository.update(asset, dto);
|
||||||
|
|
||||||
|
return mapAsset(updatedAsset);
|
||||||
|
}
|
||||||
|
|
||||||
public async downloadFile(query: ServeFileDto, res: Res) {
|
public async downloadFile(query: ServeFileDto, res: Res) {
|
||||||
try {
|
try {
|
||||||
let fileReadStream = null;
|
let fileReadStream = null;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateAssetDto {
|
||||||
|
@IsBoolean()
|
||||||
|
isFavorite!: boolean;
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ export class UserResponseDto {
|
|||||||
profileImagePath!: string;
|
profileImagePath!: string;
|
||||||
shouldChangePassword!: boolean;
|
shouldChangePassword!: boolean;
|
||||||
isAdmin!: boolean;
|
isAdmin!: boolean;
|
||||||
|
deletedAt!: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapUser(entity: UserEntity): UserResponseDto {
|
export function mapUser(entity: UserEntity): UserResponseDto {
|
||||||
@@ -21,5 +22,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
|
|||||||
profileImagePath: entity.profileImagePath,
|
profileImagePath: entity.profileImagePath,
|
||||||
shouldChangePassword: entity.shouldChangePassword,
|
shouldChangePassword: entity.shouldChangePassword,
|
||||||
isAdmin: entity.isAdmin,
|
isAdmin: entity.isAdmin,
|
||||||
|
deletedAt: entity.deletedAt || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import * as bcrypt from 'bcrypt';
|
|||||||
import { UpdateUserDto } from './dto/update-user.dto';
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
|
||||||
export interface IUserRepository {
|
export interface IUserRepository {
|
||||||
get(userId: string): Promise<UserEntity | null>;
|
get(userId: string, withDeleted?: boolean): Promise<UserEntity | null>;
|
||||||
getByEmail(email: string): Promise<UserEntity | null>;
|
getByEmail(email: string): Promise<UserEntity | null>;
|
||||||
getList(filter?: { excludeId?: string }): Promise<UserEntity[]>;
|
getList(filter?: { excludeId?: string }): Promise<UserEntity[]>;
|
||||||
create(createUserDto: CreateUserDto): Promise<UserEntity>;
|
create(createUserDto: CreateUserDto): Promise<UserEntity>;
|
||||||
update(user: UserEntity, updateUserDto: UpdateUserDto): Promise<UserEntity>;
|
update(user: UserEntity, updateUserDto: UpdateUserDto): Promise<UserEntity>;
|
||||||
createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity>;
|
createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity>;
|
||||||
|
delete(user: UserEntity): Promise<UserEntity>;
|
||||||
|
restore(user: UserEntity): Promise<UserEntity>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const USER_REPOSITORY = 'USER_REPOSITORY';
|
export const USER_REPOSITORY = 'USER_REPOSITORY';
|
||||||
@@ -27,8 +29,8 @@ export class UserRepository implements IUserRepository {
|
|||||||
return bcrypt.hash(password, salt);
|
return bcrypt.hash(password, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(userId: string): Promise<UserEntity | null> {
|
async get(userId: string, withDeleted?: boolean): Promise<UserEntity | null> {
|
||||||
return this.userRepository.findOne({ where: { id: userId } });
|
return this.userRepository.findOne({ where: { id: userId }, withDeleted: withDeleted });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByEmail(email: string): Promise<UserEntity | null> {
|
async getByEmail(email: string): Promise<UserEntity | null> {
|
||||||
@@ -40,9 +42,10 @@ export class UserRepository implements IUserRepository {
|
|||||||
if (!excludeId) {
|
if (!excludeId) {
|
||||||
return this.userRepository.find(); // TODO: this should also be ordered the same as below
|
return this.userRepository.find(); // TODO: this should also be ordered the same as below
|
||||||
}
|
}
|
||||||
|
return this.userRepository
|
||||||
return this.userRepository.find({
|
.find({
|
||||||
where: { id: Not(excludeId) },
|
where: { id: Not(excludeId) },
|
||||||
|
withDeleted: true,
|
||||||
order: {
|
order: {
|
||||||
createdAt: 'DESC',
|
createdAt: 'DESC',
|
||||||
},
|
},
|
||||||
@@ -88,6 +91,17 @@ export class UserRepository implements IUserRepository {
|
|||||||
return this.userRepository.save(user);
|
return this.userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete(user: UserEntity): Promise<UserEntity> {
|
||||||
|
if (user.isAdmin) {
|
||||||
|
throw new BadRequestException('Cannot delete admin user! stay sane!');
|
||||||
|
}
|
||||||
|
return this.userRepository.softRemove(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(user: UserEntity): Promise<UserEntity> {
|
||||||
|
return this.userRepository.recover(user);
|
||||||
|
}
|
||||||
|
|
||||||
async createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity> {
|
async createProfileImage(user: UserEntity, fileInfo: Express.Multer.File): Promise<UserEntity> {
|
||||||
user.profileImagePath = fileInfo.path;
|
user.profileImagePath = fileInfo.path;
|
||||||
return this.userRepository.save(user);
|
return this.userRepository.save(user);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
ValidationPipe,
|
ValidationPipe,
|
||||||
@@ -67,6 +68,20 @@ export class UserController {
|
|||||||
return await this.userService.getUserCount();
|
return await this.userService.getUserCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authenticated({ admin: true })
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Delete('/:userId')
|
||||||
|
async deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> {
|
||||||
|
return await this.userService.deleteUser(authUser, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authenticated({ admin: true })
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Post('/:userId/restore')
|
||||||
|
async restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> {
|
||||||
|
return await this.userService.restoreUser(authUser, userId);
|
||||||
|
}
|
||||||
|
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Put()
|
@Put()
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ describe('UserService', () => {
|
|||||||
getByEmail: jest.fn(),
|
getByEmail: jest.fn(),
|
||||||
getList: jest.fn(),
|
getList: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
|
restore: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sui = new UserService(userRepositoryMock);
|
sui = new UserService(userRepositoryMock);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
@@ -38,8 +40,8 @@ export class UserService {
|
|||||||
return allUserExceptRequestedUser.map(mapUser);
|
return allUserExceptRequestedUser.map(mapUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserById(userId: string): Promise<UserResponseDto> {
|
async getUserById(userId: string, withDeleted = false): Promise<UserResponseDto> {
|
||||||
const user = await this.userRepository.get(userId);
|
const user = await this.userRepository.get(userId, withDeleted);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
@@ -105,6 +107,48 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteUser(authUser: AuthUserDto, userId: string): Promise<UserResponseDto> {
|
||||||
|
const requestor = await this.userRepository.get(authUser.id);
|
||||||
|
if (!requestor) {
|
||||||
|
throw new UnauthorizedException('Requestor not found');
|
||||||
|
}
|
||||||
|
if (!requestor.isAdmin) {
|
||||||
|
throw new ForbiddenException('Unauthorized');
|
||||||
|
}
|
||||||
|
const user = await this.userRepository.get(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestException('User not found');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const deletedUser = await this.userRepository.delete(user);
|
||||||
|
return mapUser(deletedUser);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(e, 'Failed to delete user');
|
||||||
|
throw new InternalServerErrorException('Failed to delete user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreUser(authUser: AuthUserDto, userId: string): Promise<UserResponseDto> {
|
||||||
|
const requestor = await this.userRepository.get(authUser.id);
|
||||||
|
if (!requestor) {
|
||||||
|
throw new UnauthorizedException('Requestor not found');
|
||||||
|
}
|
||||||
|
if (!requestor.isAdmin) {
|
||||||
|
throw new ForbiddenException('Unauthorized');
|
||||||
|
}
|
||||||
|
const user = await this.userRepository.get(userId, true);
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestException('User not found');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const restoredUser = await this.userRepository.restore(user);
|
||||||
|
return mapUser(restoredUser);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(e, 'Failed to restore deleted user');
|
||||||
|
throw new InternalServerErrorException('Failed to restore deleted user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createProfileImage(
|
async createProfileImage(
|
||||||
authUser: AuthUserDto,
|
authUser: AuthUserDto,
|
||||||
fileInfo: Express.Multer.File,
|
fileInfo: Express.Multer.File,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface IServerVersion {
|
|||||||
|
|
||||||
export const serverVersion: IServerVersion = {
|
export const serverVersion: IServerVersion = {
|
||||||
major: 1,
|
major: 1,
|
||||||
minor: 34,
|
minor: 35,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
build: 53,
|
build: 54,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import path from 'path';
|
|||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { serverVersion } from './constants/server_version.constant';
|
import { serverVersion } from './constants/server_version.constant';
|
||||||
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
||||||
|
import { json } from 'body-parser';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
|
|
||||||
app.set('trust proxy');
|
app.set('trust proxy');
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
app.use(json({ limit: '10mb' }));
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
app.enableCors();
|
app.enableCors();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { Process, Processor } from '@nestjs/bull';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||||
import fs from 'fs';
|
|
||||||
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
||||||
import { Job } from 'bull';
|
import { Job } from 'bull';
|
||||||
import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
|
import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto';
|
||||||
|
import { assetUtils } from '@app/common/utils';
|
||||||
|
|
||||||
@Processor('background-task')
|
@Processor('background-task')
|
||||||
export class BackgroundTaskProcessor {
|
export class BackgroundTaskProcessor {
|
||||||
@@ -23,37 +23,7 @@ export class BackgroundTaskProcessor {
|
|||||||
const { assets } = job.data;
|
const { assets } = job.data;
|
||||||
|
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
fs.unlink(asset.originalPath, (err) => {
|
assetUtils.deleteFiles(asset);
|
||||||
if (err) {
|
|
||||||
console.log('error deleting ', asset.originalPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: what if there is no asset.resizePath. Should fail the Job?
|
|
||||||
// => panoti report: Job not fail
|
|
||||||
if (asset.resizePath) {
|
|
||||||
fs.unlink(asset.resizePath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log('error deleting ', asset.resizePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (asset.webpPath) {
|
|
||||||
fs.unlink(asset.webpPath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log('error deleting ', asset.webpPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (asset.encodedVideoPath) {
|
|
||||||
fs.unlink(asset.encodedVideoPath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log('error deleting ', asset.encodedVideoPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,19 @@ import { AssetEntity } from '@app/database/entities/asset.entity';
|
|||||||
import { ScheduleTasksService } from './schedule-tasks.service';
|
import { ScheduleTasksService } from './schedule-tasks.service';
|
||||||
import { QueueNameEnum } from '@app/job/constants/queue-name.constant';
|
import { QueueNameEnum } from '@app/job/constants/queue-name.constant';
|
||||||
import { ExifEntity } from '@app/database/entities/exif.entity';
|
import { ExifEntity } from '@app/database/entities/exif.entity';
|
||||||
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
|
TypeOrmModule.forFeature([AssetEntity, ExifEntity, UserEntity]),
|
||||||
|
BullModule.registerQueue({
|
||||||
|
name: QueueNameEnum.USER_DELETION,
|
||||||
|
defaultJobOptions: {
|
||||||
|
attempts: 3,
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: QueueNameEnum.VIDEO_CONVERSION,
|
name: QueueNameEnum.VIDEO_CONVERSION,
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Queue } from 'bull';
|
|||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { ExifEntity } from '@app/database/entities/exif.entity';
|
import { ExifEntity } from '@app/database/entities/exif.entity';
|
||||||
import {
|
import {
|
||||||
|
userDeletionProcessorName,
|
||||||
exifExtractionProcessorName,
|
exifExtractionProcessorName,
|
||||||
generateWEBPThumbnailProcessorName,
|
generateWEBPThumbnailProcessorName,
|
||||||
IMetadataExtractionJob,
|
IMetadataExtractionJob,
|
||||||
@@ -18,10 +19,16 @@ import {
|
|||||||
videoMetadataExtractionProcessorName,
|
videoMetadataExtractionProcessorName,
|
||||||
} from '@app/job';
|
} from '@app/job';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
|
import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface';
|
||||||
|
import { userUtils } from '@app/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ScheduleTasksService {
|
export class ScheduleTasksService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@InjectRepository(UserEntity)
|
||||||
|
private userRepository: Repository<UserEntity>,
|
||||||
|
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
|
|
||||||
@@ -37,6 +44,9 @@ export class ScheduleTasksService {
|
|||||||
@InjectQueue(QueueNameEnum.METADATA_EXTRACTION)
|
@InjectQueue(QueueNameEnum.METADATA_EXTRACTION)
|
||||||
private metadataExtractionQueue: Queue<IMetadataExtractionJob>,
|
private metadataExtractionQueue: Queue<IMetadataExtractionJob>,
|
||||||
|
|
||||||
|
@InjectQueue(QueueNameEnum.USER_DELETION)
|
||||||
|
private userDeletionQueue: Queue<IUserDeletionJob>,
|
||||||
|
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -128,4 +138,14 @@ export class ScheduleTasksService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Cron(CronExpression.EVERY_DAY_AT_11PM)
|
||||||
|
async deleteUserAndRelatedAssets() {
|
||||||
|
const usersToDelete = await this.userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } });
|
||||||
|
for (const user of usersToDelete) {
|
||||||
|
if (userUtils.isReadyForDeletion(user)) {
|
||||||
|
await this.userDeletionQueue.add(userDeletionProcessorName, { user: user }, { jobId: randomUUID() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ describe('User', () => {
|
|||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: userTwoEmail,
|
email: userTwoEmail,
|
||||||
@@ -114,6 +115,7 @@ describe('User', () => {
|
|||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { APP_UPLOAD_LOCATION, userUtils } from '@app/common';
|
||||||
|
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||||
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
|
import { QueueNameEnum, userDeletionProcessorName } from '@app/job';
|
||||||
|
import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface';
|
||||||
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Job } from 'bull';
|
||||||
|
import { join } from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Processor(QueueNameEnum.USER_DELETION)
|
||||||
|
export class UserDeletionProcessor {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(UserEntity)
|
||||||
|
private userRepository: Repository<UserEntity>,
|
||||||
|
|
||||||
|
@InjectRepository(AssetEntity)
|
||||||
|
private assetRepository: Repository<AssetEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Process(userDeletionProcessorName)
|
||||||
|
async processUserDeletion(job: Job<IUserDeletionJob>) {
|
||||||
|
const { user } = job.data;
|
||||||
|
// just for extra protection here
|
||||||
|
if (userUtils.isReadyForDeletion(user)) {
|
||||||
|
const basePath = APP_UPLOAD_LOCATION;
|
||||||
|
const userAssetDir = join(basePath, user.id)
|
||||||
|
fs.rmSync(userAssetDir, { recursive: true, force: true })
|
||||||
|
await this.assetRepository.delete({ userId: user.id })
|
||||||
|
await this.userRepository.remove(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
39
server/libs/common/src/utils/asset-utils.ts
Normal file
39
server/libs/common/src/utils/asset-utils.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||||
|
import { AssetResponseDto } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const deleteFiles = (asset: AssetEntity | AssetResponseDto) => {
|
||||||
|
fs.unlink(asset.originalPath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log('error deleting ', asset.originalPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: what if there is no asset.resizePath. Should fail the Job?
|
||||||
|
// => panoti report: Job not fail
|
||||||
|
if (asset.resizePath) {
|
||||||
|
fs.unlink(asset.resizePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log('error deleting ', asset.resizePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.webpPath) {
|
||||||
|
fs.unlink(asset.webpPath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log('error deleting ', asset.webpPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.encodedVideoPath) {
|
||||||
|
fs.unlink(asset.encodedVideoPath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log('error deleting ', asset.encodedVideoPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const assetUtils = { deleteFiles };
|
||||||
@@ -1 +1,3 @@
|
|||||||
export * from './time-utils';
|
export * from './time-utils';
|
||||||
|
export * from './asset-utils';
|
||||||
|
export * from './user-utils';
|
||||||
|
|||||||
19
server/libs/common/src/utils/user-utils.spec.ts
Normal file
19
server/libs/common/src/utils/user-utils.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// create unit test for user utils
|
||||||
|
|
||||||
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
|
import { userUtils } from './user-utils';
|
||||||
|
|
||||||
|
describe('User Utilities', () => {
|
||||||
|
describe('checkIsReadyForDeletion', () => {
|
||||||
|
it('check that user is not ready to be deleted', () => {
|
||||||
|
const result = userUtils.isReadyForDeletion({ deletedAt: new Date() } as UserEntity);
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check that user is ready to be deleted', () => {
|
||||||
|
const aWeekAgo = new Date(new Date().getTime() - 8 * 86400000);
|
||||||
|
const result = userUtils.isReadyForDeletion({ deletedAt: aWeekAgo } as UserEntity);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
16
server/libs/common/src/utils/user-utils.ts
Normal file
16
server/libs/common/src/utils/user-utils.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
|
|
||||||
|
function createUserUtils() {
|
||||||
|
const isReadyForDeletion = (user: UserEntity): boolean => {
|
||||||
|
if (user.deletedAt == null) return false;
|
||||||
|
const millisecondsInDay = 86400000;
|
||||||
|
// get this number (7 days) from some configuration perhaps ?
|
||||||
|
const millisecondsDeleteWait = millisecondsInDay * 7;
|
||||||
|
|
||||||
|
const millisecondsSinceDelete = new Date().getTime() - (user.deletedAt?.getTime() ?? 0);
|
||||||
|
return millisecondsSinceDelete >= millisecondsDeleteWait;
|
||||||
|
};
|
||||||
|
return { isReadyForDeletion };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userUtils = createUserUtils();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('users')
|
@Entity('users')
|
||||||
export class UserEntity {
|
export class UserEntity {
|
||||||
@@ -31,4 +31,7 @@ export class UserEntity {
|
|||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: string;
|
createdAt!: string;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddingDeletedAtColumnInUserEntity1667762360744 implements MigrationInterface {
|
||||||
|
name = 'AddingDeletedAtColumnInUserEntity1667762360744';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "deletedAt" TIMESTAMP`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "deletedAt"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,3 +29,8 @@ export enum MachineLearningJobNameEnum {
|
|||||||
OBJECT_DETECTION = 'detect-object',
|
OBJECT_DETECTION = 'detect-object',
|
||||||
IMAGE_TAGGING = 'tag-image',
|
IMAGE_TAGGING = 'tag-image',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User deletion Queue Jobs
|
||||||
|
*/
|
||||||
|
export const userDeletionProcessorName = 'user-deletion';
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export enum QueueNameEnum {
|
|||||||
CHECKSUM_GENERATION = 'generate-checksum-queue',
|
CHECKSUM_GENERATION = 'generate-checksum-queue',
|
||||||
ASSET_UPLOADED = 'asset-uploaded-queue',
|
ASSET_UPLOADED = 'asset-uploaded-queue',
|
||||||
MACHINE_LEARNING = 'machine-learning-queue',
|
MACHINE_LEARNING = 'machine-learning-queue',
|
||||||
|
USER_DELETION = 'user-deletion-queue',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
|
|
||||||
|
export interface IUserDeletionJob {
|
||||||
|
/**
|
||||||
|
* The user entity that was saved in the database
|
||||||
|
*/
|
||||||
|
user: UserEntity;
|
||||||
|
}
|
||||||
@@ -1391,6 +1391,19 @@ export interface UpdateAlbumDto {
|
|||||||
*/
|
*/
|
||||||
'albumThumbnailAssetId'?: string;
|
'albumThumbnailAssetId'?: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface UpdateAssetDto
|
||||||
|
*/
|
||||||
|
export interface UpdateAssetDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'isFavorite': boolean;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@@ -1575,6 +1588,12 @@ export interface UserResponseDto {
|
|||||||
* @memberof UserResponseDto
|
* @memberof UserResponseDto
|
||||||
*/
|
*/
|
||||||
'isAdmin': boolean;
|
'isAdmin': boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UserResponseDto
|
||||||
|
*/
|
||||||
|
'deletedAt': string | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -3052,6 +3071,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Update an asset
|
||||||
|
* @summary
|
||||||
|
* @param {string} assetId
|
||||||
|
* @param {UpdateAssetDto} updateAssetDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
updateAssetById: async (assetId: string, updateAssetDto: UpdateAssetDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'assetId' is not null or undefined
|
||||||
|
assertParamExists('updateAssetById', 'assetId', assetId)
|
||||||
|
// verify required parameter 'updateAssetDto' is not null or undefined
|
||||||
|
assertParamExists('updateAssetById', 'updateAssetDto', updateAssetDto)
|
||||||
|
const localVarPath = `/asset/assetById/{assetId}`
|
||||||
|
.replace(`{${"assetId"}}`, encodeURIComponent(String(assetId)));
|
||||||
|
// 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: 'PUT', ...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(updateAssetDto, localVarRequestOptions, configuration)
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {any} assetData
|
* @param {any} assetData
|
||||||
@@ -3273,6 +3336,18 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.serveFile(aid, did, isThumb, isWeb, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.serveFile(aid, did, isThumb, isWeb, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Update an asset
|
||||||
|
* @summary
|
||||||
|
* @param {string} assetId
|
||||||
|
* @param {UpdateAssetDto} updateAssetDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async updateAssetById(assetId: string, updateAssetDto: UpdateAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetResponseDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssetById(assetId, updateAssetDto, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {any} assetData
|
* @param {any} assetData
|
||||||
@@ -3444,6 +3519,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
serveFile(aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
|
serveFile(aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
|
||||||
return localVarFp.serveFile(aid, did, isThumb, isWeb, options).then((request) => request(axios, basePath));
|
return localVarFp.serveFile(aid, did, isThumb, isWeb, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Update an asset
|
||||||
|
* @summary
|
||||||
|
* @param {string} assetId
|
||||||
|
* @param {UpdateAssetDto} updateAssetDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
updateAssetById(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> {
|
||||||
|
return localVarFp.updateAssetById(assetId, updateAssetDto, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {any} assetData
|
* @param {any} assetData
|
||||||
@@ -3646,6 +3732,19 @@ export class AssetApi extends BaseAPI {
|
|||||||
return AssetApiFp(this.configuration).serveFile(aid, did, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).serveFile(aid, did, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an asset
|
||||||
|
* @summary
|
||||||
|
* @param {string} assetId
|
||||||
|
* @param {UpdateAssetDto} updateAssetDto
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof AssetApi
|
||||||
|
*/
|
||||||
|
public updateAssetById(assetId: string, updateAssetDto: UpdateAssetDto, options?: AxiosRequestConfig) {
|
||||||
|
return AssetApiFp(this.configuration).updateAssetById(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {any} assetData
|
* @param {any} assetData
|
||||||
@@ -4711,6 +4810,43 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
|
|||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
deleteUser: async (userId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'userId' is not null or undefined
|
||||||
|
assertParamExists('deleteUser', 'userId', userId)
|
||||||
|
const localVarPath = `/user/{userId}`
|
||||||
|
.replace(`{${"userId"}}`, encodeURIComponent(String(userId)));
|
||||||
|
// 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: 'DELETE', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication bearer required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {boolean} isAll
|
* @param {boolean} isAll
|
||||||
@@ -4870,6 +5006,43 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
restoreUser: async (userId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'userId' is not null or undefined
|
||||||
|
assertParamExists('restoreUser', 'userId', userId)
|
||||||
|
const localVarPath = `/user/{userId}/restore`
|
||||||
|
.replace(`{${"userId"}}`, encodeURIComponent(String(userId)));
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
@@ -4948,6 +5121,16 @@ export const UserApiFp = function(configuration?: Configuration) {
|
|||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async deleteUser(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUser(userId, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {boolean} isAll
|
* @param {boolean} isAll
|
||||||
@@ -4996,6 +5179,16 @@ export const UserApiFp = function(configuration?: Configuration) {
|
|||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCount(options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async restoreUser(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.restoreUser(userId, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {UpdateUserDto} updateUserDto
|
* @param {UpdateUserDto} updateUserDto
|
||||||
@@ -5034,6 +5227,15 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
|
|||||||
createUser(createUserDto: CreateUserDto, options?: any): AxiosPromise<UserResponseDto> {
|
createUser(createUserDto: CreateUserDto, options?: any): AxiosPromise<UserResponseDto> {
|
||||||
return localVarFp.createUser(createUserDto, options).then((request) => request(axios, basePath));
|
return localVarFp.createUser(createUserDto, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
deleteUser(userId: string, options?: any): AxiosPromise<UserResponseDto> {
|
||||||
|
return localVarFp.deleteUser(userId, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {boolean} isAll
|
* @param {boolean} isAll
|
||||||
@@ -5077,6 +5279,15 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
|
|||||||
getUserCount(options?: any): AxiosPromise<UserCountResponseDto> {
|
getUserCount(options?: any): AxiosPromise<UserCountResponseDto> {
|
||||||
return localVarFp.getUserCount(options).then((request) => request(axios, basePath));
|
return localVarFp.getUserCount(options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
restoreUser(userId: string, options?: any): AxiosPromise<UserResponseDto> {
|
||||||
|
return localVarFp.restoreUser(userId, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {UpdateUserDto} updateUserDto
|
* @param {UpdateUserDto} updateUserDto
|
||||||
@@ -5118,6 +5329,17 @@ export class UserApi extends BaseAPI {
|
|||||||
return UserApiFp(this.configuration).createUser(createUserDto, options).then((request) => request(this.axios, this.basePath));
|
return UserApiFp(this.configuration).createUser(createUserDto, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof UserApi
|
||||||
|
*/
|
||||||
|
public deleteUser(userId: string, options?: AxiosRequestConfig) {
|
||||||
|
return UserApiFp(this.configuration).deleteUser(userId, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {boolean} isAll
|
* @param {boolean} isAll
|
||||||
@@ -5171,6 +5393,17 @@ export class UserApi extends BaseAPI {
|
|||||||
return UserApiFp(this.configuration).getUserCount(options).then((request) => request(this.axios, this.basePath));
|
return UserApiFp(this.configuration).getUserCount(options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof UserApi
|
||||||
|
*/
|
||||||
|
public restoreUser(userId: string, options?: AxiosRequestConfig) {
|
||||||
|
return UserApiFp(this.configuration).restoreUser(userId, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {UpdateUserDto} updateUserDto
|
* @param {UpdateUserDto} updateUserDto
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { api, UserResponseDto } from '@api';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const deleteUser = async () => {
|
||||||
|
const deletedUser = await api.userApi.deleteUser(user.id);
|
||||||
|
if (deletedUser.data.deletedAt != null) dispatch('user-delete-success');
|
||||||
|
else dispatch('user-delete-fail');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||||
|
>
|
||||||
|
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||||
|
Confirm User Deletion
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="ml-4 text-md py-5 text-center">
|
||||||
|
{user.firstName}
|
||||||
|
{user.lastName} account and assets along will be marked to delete completely after 7 days. are
|
||||||
|
you sure you want to proceed ?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex w-full px-4 gap-4 mt-8">
|
||||||
|
<button
|
||||||
|
on:click={deleteUser}
|
||||||
|
class="flex-1 transition-colors bg-red-500 hover:bg-red-400 px-6 py-3 text-white rounded-full w-full font-medium"
|
||||||
|
>Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
40
web/src/lib/components/admin-page/restore-dialoge.svelte
Normal file
40
web/src/lib/components/admin-page/restore-dialoge.svelte
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { api, UserResponseDto } from '@api';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const restoreUser = async () => {
|
||||||
|
const restoredUser = await api.userApi.restoreUser(user.id);
|
||||||
|
if (restoredUser.data.deletedAt == null) dispatch('user-restore-success');
|
||||||
|
else dispatch('user-restore-fail');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||||
|
>
|
||||||
|
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||||
|
Restore User
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="ml-4 text-md py-5 text-center">
|
||||||
|
{user.firstName}
|
||||||
|
{user.lastName} account will restored
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex w-full px-4 gap-4 mt-8">
|
||||||
|
<button
|
||||||
|
on:click={restoreUser}
|
||||||
|
class="flex-1 transition-colors bg-lime-600 hover:bg-lime-500 px-6 py-3 text-white rounded-full w-full font-medium"
|
||||||
|
>Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,9 +3,21 @@
|
|||||||
|
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
||||||
|
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||||
|
import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
export let allUsers: Array<UserResponseDto>;
|
export let allUsers: Array<UserResponseDto>;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const isDeleted = (user: UserResponseDto): boolean => {
|
||||||
|
return user.deletedAt != null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeleteDate = (user: UserResponseDto): string => {
|
||||||
|
return moment(user.deletedAt).add(7, 'days').format('LL');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<table class="text-left w-full my-5">
|
<table class="text-left w-full my-5">
|
||||||
@@ -16,7 +28,7 @@
|
|||||||
<th class="text-center w-1/4 font-medium text-sm">Email</th>
|
<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>
|
<th class="text-center w-1/4 font-medium text-sm">First name</th>
|
||||||
<th class="text-center w-1/4 font-medium text-sm">Last name</th>
|
<th class="text-center w-1/4 font-medium text-sm">Last name</th>
|
||||||
<th class="text-center w-1/4 font-medium text-sm">Edit</th>
|
<th class="text-center w-1/4 font-medium text-sm">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody
|
<tbody
|
||||||
@@ -25,21 +37,44 @@
|
|||||||
{#each allUsers as user, i}
|
{#each allUsers as user, i}
|
||||||
<tr
|
<tr
|
||||||
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-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]'
|
isDeleted(user)
|
||||||
|
? 'bg-red-50'
|
||||||
|
: 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>
|
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
|
||||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
|
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</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">{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
|
{#if !isDeleted(user)}
|
||||||
on:click={() => {
|
<button
|
||||||
dispatch('edit-user', { user });
|
on:click={() => {
|
||||||
}}
|
dispatch('edit-user', { user });
|
||||||
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
|
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"
|
||||||
></td
|
><PencilOutline size="16" /></button
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
dispatch('delete-user', { user });
|
||||||
|
}}
|
||||||
|
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"
|
||||||
|
><TrashCanOutline size="16" /></button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#if isDeleted(user)}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
dispatch('restore-user', { user });
|
||||||
|
}}
|
||||||
|
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"
|
||||||
|
title={`scheduled removal on ${getDeleteDate(user)}`}
|
||||||
|
><DeleteRestore size="16" /></button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -9,6 +9,14 @@
|
|||||||
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
||||||
import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
|
import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
|
||||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||||
|
import Star from 'svelte-material-icons/Star.svelte';
|
||||||
|
import StarOutline from 'svelte-material-icons/StarOutline.svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { AssetResponseDto } from '../../../api';
|
||||||
|
|
||||||
|
export let asset: AssetResponseDto;
|
||||||
|
|
||||||
|
const isOwner = asset.ownerId === $page.data.user.id;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@@ -38,8 +46,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-white flex gap-2">
|
<div class="text-white flex gap-2">
|
||||||
<CircleIconButton logo={CloudDownloadOutline} on:click={() => dispatch('download')} />
|
<CircleIconButton logo={CloudDownloadOutline} on:click={() => dispatch('download')} />
|
||||||
<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} />
|
|
||||||
<CircleIconButton logo={InformationOutline} on:click={() => dispatch('showDetail')} />
|
<CircleIconButton logo={InformationOutline} on:click={() => dispatch('showDetail')} />
|
||||||
|
{#if isOwner}
|
||||||
|
<CircleIconButton
|
||||||
|
logo={asset.isFavorite ? Star : StarOutline}
|
||||||
|
on:click={() => dispatch('favorite')}
|
||||||
|
title="Favorite"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} />
|
||||||
<CircleIconButton logo={DotsVertical} on:click={(event) => showOptionsMenu(event)} />
|
<CircleIconButton logo={DotsVertical} on:click={(event) => showOptionsMenu(event)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -178,6 +178,14 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleFavorite = async () => {
|
||||||
|
const { data } = await api.assetApi.updateAssetById(asset.id, {
|
||||||
|
isFavorite: !asset.isFavorite
|
||||||
|
});
|
||||||
|
|
||||||
|
asset.isFavorite = data.isFavorite;
|
||||||
|
};
|
||||||
|
|
||||||
const openAlbumPicker = (shared: boolean) => {
|
const openAlbumPicker = (shared: boolean) => {
|
||||||
isShowAlbumPicker = true;
|
isShowAlbumPicker = true;
|
||||||
addToSharedAlbum = shared;
|
addToSharedAlbum = shared;
|
||||||
@@ -218,10 +226,12 @@
|
|||||||
>
|
>
|
||||||
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
|
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
|
||||||
<AsserViewerNavBar
|
<AsserViewerNavBar
|
||||||
|
{asset}
|
||||||
on:goBack={closeViewer}
|
on:goBack={closeViewer}
|
||||||
on:showDetail={showDetailInfoHandler}
|
on:showDetail={showDetailInfoHandler}
|
||||||
on:download={downloadFile}
|
on:download={downloadFile}
|
||||||
on:delete={deleteAsset}
|
on:delete={deleteAsset}
|
||||||
|
on:favorite={toggleFavorite}
|
||||||
on:addToAlbum={() => openAlbumPicker(false)}
|
on:addToAlbum={() => openAlbumPicker(false)}
|
||||||
on:addToSharedAlbum={() => openAlbumPicker(true)}
|
on:addToSharedAlbum={() => openAlbumPicker(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,21 +11,25 @@
|
|||||||
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 EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||||
|
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
|
||||||
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { api, ServerStatsResponseDto, UserResponseDto } from '@api';
|
import { api, ServerStatsResponseDto, UserResponseDto } from '@api';
|
||||||
import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
|
import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
|
||||||
import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
|
import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
|
||||||
|
import RestoreDialoge from '$lib/components/admin-page/restore-dialoge.svelte';
|
||||||
|
|
||||||
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
|
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
let editUser: UserResponseDto;
|
let selectedUser: UserResponseDto;
|
||||||
|
|
||||||
let shouldShowEditUserForm = false;
|
let shouldShowEditUserForm = false;
|
||||||
let shouldShowCreateUserForm = false;
|
let shouldShowCreateUserForm = false;
|
||||||
let shouldShowInfoPanel = false;
|
let shouldShowInfoPanel = false;
|
||||||
|
let shouldShowDeleteConfirmDialog = false;
|
||||||
|
let shouldShowRestoreDialog = false;
|
||||||
let serverStat: ServerStatsResponseDto;
|
let serverStat: ServerStatsResponseDto;
|
||||||
|
|
||||||
const onButtonClicked = (buttonType: CustomEvent) => {
|
const onButtonClicked = (buttonType: CustomEvent) => {
|
||||||
@@ -45,7 +49,7 @@
|
|||||||
|
|
||||||
const editUserHandler = async (event: CustomEvent) => {
|
const editUserHandler = async (event: CustomEvent) => {
|
||||||
const { user } = event.detail;
|
const { user } = event.detail;
|
||||||
editUser = user;
|
selectedUser = user;
|
||||||
shouldShowEditUserForm = true;
|
shouldShowEditUserForm = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,6 +66,43 @@
|
|||||||
shouldShowInfoPanel = true;
|
shouldShowInfoPanel = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteUserHandler = async (event: CustomEvent) => {
|
||||||
|
const { user } = event.detail;
|
||||||
|
selectedUser = user;
|
||||||
|
shouldShowDeleteConfirmDialog = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUserDeleteSuccess = async () => {
|
||||||
|
const getAllUsersRes = await api.userApi.getAllUsers(false);
|
||||||
|
data.allUsers = getAllUsersRes.data;
|
||||||
|
shouldShowDeleteConfirmDialog = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUserDeleteFail = async () => {
|
||||||
|
const getAllUsersRes = await api.userApi.getAllUsers(false);
|
||||||
|
data.allUsers = getAllUsersRes.data;
|
||||||
|
shouldShowDeleteConfirmDialog = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreUserHandler = async (event: CustomEvent) => {
|
||||||
|
const { user } = event.detail;
|
||||||
|
selectedUser = user;
|
||||||
|
shouldShowRestoreDialog = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUserRestoreSuccess = async () => {
|
||||||
|
const getAllUsersRes = await api.userApi.getAllUsers(false);
|
||||||
|
data.allUsers = getAllUsersRes.data;
|
||||||
|
shouldShowRestoreDialog = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUserRestoreFail = async () => {
|
||||||
|
// show fail dialog
|
||||||
|
const getAllUsersRes = await api.userApi.getAllUsers(false);
|
||||||
|
data.allUsers = getAllUsersRes.data;
|
||||||
|
shouldShowRestoreDialog = false;
|
||||||
|
};
|
||||||
|
|
||||||
const getServerStats = async () => {
|
const getServerStats = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.serverInfoApi.getStats();
|
const res = await api.serverInfoApi.getStats();
|
||||||
@@ -87,13 +128,33 @@
|
|||||||
{#if shouldShowEditUserForm}
|
{#if shouldShowEditUserForm}
|
||||||
<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
|
<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
|
||||||
<EditUserForm
|
<EditUserForm
|
||||||
user={editUser}
|
user={selectedUser}
|
||||||
on:edit-success={onEditUserSuccess}
|
on:edit-success={onEditUserSuccess}
|
||||||
on:reset-password-success={onEditPasswordSuccess}
|
on:reset-password-success={onEditPasswordSuccess}
|
||||||
/>
|
/>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if shouldShowDeleteConfirmDialog}
|
||||||
|
<FullScreenModal on:clickOutside={() => (shouldShowDeleteConfirmDialog = false)}>
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
user={selectedUser}
|
||||||
|
on:user-delete-success={onUserDeleteSuccess}
|
||||||
|
on:user-delete-fail={onUserDeleteFail}
|
||||||
|
/>
|
||||||
|
</FullScreenModal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if shouldShowRestoreDialog}
|
||||||
|
<FullScreenModal on:clickOutside={() => (shouldShowRestoreDialog = false)}>
|
||||||
|
<RestoreDialoge
|
||||||
|
user={selectedUser}
|
||||||
|
on:user-restore-success={onUserRestoreSuccess}
|
||||||
|
on:user-restore-fail={onUserRestoreFail}
|
||||||
|
/>
|
||||||
|
</FullScreenModal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if shouldShowInfoPanel}
|
{#if shouldShowInfoPanel}
|
||||||
<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
|
<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
|
||||||
<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
|
<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
|
||||||
@@ -160,6 +221,8 @@
|
|||||||
allUsers={data.allUsers}
|
allUsers={data.allUsers}
|
||||||
on:create-user={() => (shouldShowCreateUserForm = true)}
|
on:create-user={() => (shouldShowCreateUserForm = true)}
|
||||||
on:edit-user={editUserHandler}
|
on:edit-user={editUserHandler}
|
||||||
|
on:delete-user={deleteUserHandler}
|
||||||
|
on:restore-user={restoreUserHandler}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedAction === AdminSideBarSelection.JOBS}
|
{#if selectedAction === AdminSideBarSelection.JOBS}
|
||||||
|
|||||||
Reference in New Issue
Block a user