mirror of
https://github.com/immich-app/immich.git
synced 2025-04-24 13:16:41 +02:00
Add information for uploading asset and error indication with error message for each failed upload. (#315)
* Added info box * Fixed upload endpoint doesn't report error status code * Added chip to show update error * Added chip to show failed upload * Add duplication check for upload * Better duplication-checking placement * Remove check for duplicated asset * Added failed backup status route * added page * Display error card with thumbnail * Improved styling * Set thumbnail with better quality * Remove force upload error
This commit is contained in:
parent
357f7d1c31
commit
58ec7553ea
@ -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.16.1"
|
version_number: "1.17.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||||
|
|
||||||
enum BackUpProgressEnum { idle, inProgress, done }
|
enum BackUpProgressEnum { idle, inProgress, done }
|
||||||
|
|
||||||
class BackUpState extends Equatable {
|
class BackUpState {
|
||||||
// enum
|
// enum
|
||||||
final BackUpProgressEnum backupProgress;
|
final BackUpProgressEnum backupProgress;
|
||||||
final List<String> allAssetsInDatabase;
|
final List<String> allAssetsInDatabase;
|
||||||
@ -26,6 +27,9 @@ class BackUpState extends Equatable {
|
|||||||
/// All assets from the selected albums that have been backup
|
/// All assets from the selected albums that have been backup
|
||||||
final Set<String> selectedAlbumsBackupAssetsIds;
|
final Set<String> selectedAlbumsBackupAssetsIds;
|
||||||
|
|
||||||
|
// Current Backup Asset
|
||||||
|
final CurrentUploadAsset currentUploadAsset;
|
||||||
|
|
||||||
const BackUpState({
|
const BackUpState({
|
||||||
required this.backupProgress,
|
required this.backupProgress,
|
||||||
required this.allAssetsInDatabase,
|
required this.allAssetsInDatabase,
|
||||||
@ -37,6 +41,7 @@ class BackUpState extends Equatable {
|
|||||||
required this.excludedBackupAlbums,
|
required this.excludedBackupAlbums,
|
||||||
required this.allUniqueAssets,
|
required this.allUniqueAssets,
|
||||||
required this.selectedAlbumsBackupAssetsIds,
|
required this.selectedAlbumsBackupAssetsIds,
|
||||||
|
required this.currentUploadAsset,
|
||||||
});
|
});
|
||||||
|
|
||||||
BackUpState copyWith({
|
BackUpState copyWith({
|
||||||
@ -50,6 +55,7 @@ class BackUpState extends Equatable {
|
|||||||
Set<AssetPathEntity>? excludedBackupAlbums,
|
Set<AssetPathEntity>? excludedBackupAlbums,
|
||||||
Set<AssetEntity>? allUniqueAssets,
|
Set<AssetEntity>? allUniqueAssets,
|
||||||
Set<String>? selectedAlbumsBackupAssetsIds,
|
Set<String>? selectedAlbumsBackupAssetsIds,
|
||||||
|
CurrentUploadAsset? currentUploadAsset,
|
||||||
}) {
|
}) {
|
||||||
return BackUpState(
|
return BackUpState(
|
||||||
backupProgress: backupProgress ?? this.backupProgress,
|
backupProgress: backupProgress ?? this.backupProgress,
|
||||||
@ -63,27 +69,47 @@ class BackUpState extends Equatable {
|
|||||||
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
|
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
|
||||||
selectedAlbumsBackupAssetsIds:
|
selectedAlbumsBackupAssetsIds:
|
||||||
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
|
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
|
||||||
|
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
|
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props {
|
bool operator ==(Object other) {
|
||||||
return [
|
if (identical(this, other)) return true;
|
||||||
backupProgress,
|
final collectionEquals = const DeepCollectionEquality().equals;
|
||||||
allAssetsInDatabase,
|
|
||||||
progressInPercentage,
|
return other is BackUpState &&
|
||||||
cancelToken,
|
other.backupProgress == backupProgress &&
|
||||||
serverInfo,
|
collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) &&
|
||||||
availableAlbums,
|
other.progressInPercentage == progressInPercentage &&
|
||||||
selectedBackupAlbums,
|
other.cancelToken == cancelToken &&
|
||||||
excludedBackupAlbums,
|
other.serverInfo == serverInfo &&
|
||||||
allUniqueAssets,
|
collectionEquals(other.availableAlbums, availableAlbums) &&
|
||||||
selectedAlbumsBackupAssetsIds,
|
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
|
||||||
];
|
collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
|
||||||
|
collectionEquals(other.allUniqueAssets, allUniqueAssets) &&
|
||||||
|
collectionEquals(other.selectedAlbumsBackupAssetsIds,
|
||||||
|
selectedAlbumsBackupAssetsIds) &&
|
||||||
|
other.currentUploadAsset == currentUploadAsset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return backupProgress.hashCode ^
|
||||||
|
allAssetsInDatabase.hashCode ^
|
||||||
|
progressInPercentage.hashCode ^
|
||||||
|
cancelToken.hashCode ^
|
||||||
|
serverInfo.hashCode ^
|
||||||
|
availableAlbums.hashCode ^
|
||||||
|
selectedBackupAlbums.hashCode ^
|
||||||
|
excludedBackupAlbums.hashCode ^
|
||||||
|
allUniqueAssets.hashCode ^
|
||||||
|
selectedAlbumsBackupAssetsIds.hashCode ^
|
||||||
|
currentUploadAsset.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class CheckDuplicateAssetResponse {
|
||||||
|
final bool isExist;
|
||||||
|
CheckDuplicateAssetResponse({
|
||||||
|
required this.isExist,
|
||||||
|
});
|
||||||
|
|
||||||
|
CheckDuplicateAssetResponse copyWith({
|
||||||
|
bool? isExist,
|
||||||
|
}) {
|
||||||
|
return CheckDuplicateAssetResponse(
|
||||||
|
isExist: isExist ?? this.isExist,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'isExist': isExist});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CheckDuplicateAssetResponse.fromMap(Map<String, dynamic> map) {
|
||||||
|
return CheckDuplicateAssetResponse(
|
||||||
|
isExist: map['isExist'] ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory CheckDuplicateAssetResponse.fromJson(String source) =>
|
||||||
|
CheckDuplicateAssetResponse.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'CheckDuplicateAssetResponse(isExist: $isExist)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is CheckDuplicateAssetResponse && other.isExist == isExist;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => isExist.hashCode;
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class CurrentUploadAsset {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final String fileName;
|
||||||
|
final String fileType;
|
||||||
|
|
||||||
|
CurrentUploadAsset({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileType,
|
||||||
|
});
|
||||||
|
|
||||||
|
CurrentUploadAsset copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
String? fileName,
|
||||||
|
String? fileType,
|
||||||
|
}) {
|
||||||
|
return CurrentUploadAsset(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
fileName: fileName ?? this.fileName,
|
||||||
|
fileType: fileType ?? this.fileType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'id': id});
|
||||||
|
result.addAll({'createdAt': createdAt.millisecondsSinceEpoch});
|
||||||
|
result.addAll({'fileName': fileName});
|
||||||
|
result.addAll({'fileType': fileType});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CurrentUploadAsset.fromMap(Map<String, dynamic> map) {
|
||||||
|
return CurrentUploadAsset(
|
||||||
|
id: map['id'] ?? '',
|
||||||
|
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']),
|
||||||
|
fileName: map['fileName'] ?? '',
|
||||||
|
fileType: map['fileType'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory CurrentUploadAsset.fromJson(String source) =>
|
||||||
|
CurrentUploadAsset.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CurrentUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is CurrentUploadAsset &&
|
||||||
|
other.id == id &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.fileName == fileName &&
|
||||||
|
other.fileType == fileType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
fileName.hashCode ^
|
||||||
|
fileType.hashCode;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class ErrorUploadAsset extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final String fileName;
|
||||||
|
final String fileType;
|
||||||
|
final AssetEntity asset;
|
||||||
|
final String errorMessage;
|
||||||
|
|
||||||
|
const ErrorUploadAsset({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileType,
|
||||||
|
required this.asset,
|
||||||
|
required this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
ErrorUploadAsset copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
String? fileName,
|
||||||
|
String? fileType,
|
||||||
|
AssetEntity? asset,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return ErrorUploadAsset(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
fileName: fileName ?? this.fileName,
|
||||||
|
fileType: fileType ?? this.fileType,
|
||||||
|
asset: asset ?? this.asset,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ErrorUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props {
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
fileName,
|
||||||
|
fileType,
|
||||||
|
errorMessage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
@ -14,8 +17,12 @@ import 'package:immich_mobile/shared/services/server_info.service.dart';
|
|||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
BackupNotifier(this._backupService, this._serverInfoService, this._authState)
|
BackupNotifier(
|
||||||
: super(
|
this._backupService,
|
||||||
|
this._serverInfoService,
|
||||||
|
this._authState,
|
||||||
|
this.ref,
|
||||||
|
) : super(
|
||||||
BackUpState(
|
BackUpState(
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
allAssetsInDatabase: const [],
|
allAssetsInDatabase: const [],
|
||||||
@ -35,6 +42,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
excludedBackupAlbums: const {},
|
excludedBackupAlbums: const {},
|
||||||
allUniqueAssets: const {},
|
allUniqueAssets: const {},
|
||||||
selectedAlbumsBackupAssetsIds: const {},
|
selectedAlbumsBackupAssetsIds: const {},
|
||||||
|
currentUploadAsset: CurrentUploadAsset(
|
||||||
|
id: '...',
|
||||||
|
createdAt: DateTime.parse('2020-10-04'),
|
||||||
|
fileName: '...',
|
||||||
|
fileType: '...',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
getBackupInfo();
|
getBackupInfo();
|
||||||
@ -43,6 +56,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final BackupService _backupService;
|
final BackupService _backupService;
|
||||||
final ServerInfoService _serverInfoService;
|
final ServerInfoService _serverInfoService;
|
||||||
final AuthenticationState _authState;
|
final AuthenticationState _authState;
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// UI INTERACTION
|
/// UI INTERACTION
|
||||||
@ -235,8 +249,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// and then update the UI according to those information
|
/// and then update the UI according to those information
|
||||||
///
|
///
|
||||||
Future<void> getBackupInfo() async {
|
Future<void> getBackupInfo() async {
|
||||||
await _getBackupAlbumsInfo();
|
await Future.wait([
|
||||||
await _updateServerInfo();
|
_getBackupAlbumsInfo(),
|
||||||
|
_updateServerInfo(),
|
||||||
|
]);
|
||||||
|
|
||||||
await _updateBackupAssetCount();
|
await _updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,13 +304,27 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Perform Backup
|
// Perform Backup
|
||||||
state = state.copyWith(cancelToken: CancellationToken());
|
state = state.copyWith(cancelToken: CancellationToken());
|
||||||
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken,
|
_backupService.backupAsset(
|
||||||
_onAssetUploaded, _onUploadProgress);
|
assetsWillBeBackup,
|
||||||
|
state.cancelToken,
|
||||||
|
_onAssetUploaded,
|
||||||
|
_onUploadProgress,
|
||||||
|
_onSetCurrentBackupAsset,
|
||||||
|
_onBackupError,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
PhotoManager.openSetting();
|
PhotoManager.openSetting();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||||
|
ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
||||||
|
state = state.copyWith(currentUploadAsset: currentUploadAsset);
|
||||||
|
}
|
||||||
|
|
||||||
void cancelBackup() {
|
void cancelBackup() {
|
||||||
state.cancelToken.cancel();
|
state.cancelToken.cancel();
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@ -375,5 +406,6 @@ final backupProvider =
|
|||||||
ref.watch(backupServiceProvider),
|
ref.watch(backupServiceProvider),
|
||||||
ref.watch(serverInfoServiceProvider),
|
ref.watch(serverInfoServiceProvider),
|
||||||
ref.watch(authenticationProvider),
|
ref.watch(authenticationProvider),
|
||||||
|
ref,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
|
|
||||||
|
class ErrorBackupListNotifier extends StateNotifier<Set<ErrorUploadAsset>> {
|
||||||
|
ErrorBackupListNotifier() : super({});
|
||||||
|
|
||||||
|
add(ErrorUploadAsset errorAsset) {
|
||||||
|
state = state.union({errorAsset});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(ErrorUploadAsset errorAsset) {
|
||||||
|
state = state.difference({errorAsset});
|
||||||
|
}
|
||||||
|
|
||||||
|
empty() {
|
||||||
|
state = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final errorBackupListProvider =
|
||||||
|
StateNotifierProvider<ErrorBackupListNotifier, Set<ErrorUploadAsset>>(
|
||||||
|
(ref) => ErrorBackupListNotifier(),
|
||||||
|
);
|
@ -7,6 +7,9 @@ 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';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/check_duplicate_asset_response.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
||||||
import 'package:immich_mobile/utils/files_helper.dart';
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
@ -20,6 +23,7 @@ final backupServiceProvider =
|
|||||||
|
|
||||||
class BackupService {
|
class BackupService {
|
||||||
final NetworkService _networkService;
|
final NetworkService _networkService;
|
||||||
|
|
||||||
BackupService(this._networkService);
|
BackupService(this._networkService);
|
||||||
|
|
||||||
Future<List<String>> getDeviceBackupAsset() async {
|
Future<List<String>> getDeviceBackupAsset() async {
|
||||||
@ -32,17 +36,40 @@ class BackupService {
|
|||||||
return result.cast<String>();
|
return result.cast<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> checkDuplicateAsset(String deviceAssetId) async {
|
||||||
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Response response =
|
||||||
|
await _networkService.postRequest(url: "asset/check", data: {
|
||||||
|
"deviceId": deviceId,
|
||||||
|
"deviceAssetId": deviceAssetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var result = CheckDuplicateAssetResponse.fromJson(response.toString());
|
||||||
|
|
||||||
|
return result.isExist;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backupAsset(
|
backupAsset(
|
||||||
Set<AssetEntity> assetList,
|
Set<AssetEntity> assetList,
|
||||||
http.CancellationToken cancelToken,
|
http.CancellationToken cancelToken,
|
||||||
Function(String, String) singleAssetDoneCb,
|
Function(String, String) singleAssetDoneCb,
|
||||||
Function(int, int) uploadProgress) async {
|
Function(int, int) uploadProgressCb,
|
||||||
|
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
|
||||||
|
Function(ErrorUploadAsset) errorCb,
|
||||||
|
) async {
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
File? file;
|
File? file;
|
||||||
|
|
||||||
http.MultipartFile? thumbnailUploadData;
|
|
||||||
|
|
||||||
for (var entity in assetList) {
|
for (var entity in assetList) {
|
||||||
try {
|
try {
|
||||||
if (entity.type == AssetType.video) {
|
if (entity.type == AssetType.video) {
|
||||||
@ -74,7 +101,7 @@ class BackupService {
|
|||||||
var req = MultipartRequest(
|
var req = MultipartRequest(
|
||||||
'POST', Uri.parse('$savedEndpoint/asset/upload'),
|
'POST', Uri.parse('$savedEndpoint/asset/upload'),
|
||||||
onProgress: ((bytes, totalBytes) =>
|
onProgress: ((bytes, totalBytes) =>
|
||||||
uploadProgress(bytes, totalBytes)));
|
uploadProgressCb(bytes, totalBytes)));
|
||||||
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
|
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
|
||||||
|
|
||||||
req.fields['deviceAssetId'] = entity.id;
|
req.fields['deviceAssetId'] = entity.id;
|
||||||
@ -88,10 +115,35 @@ class BackupService {
|
|||||||
|
|
||||||
req.files.add(assetRawUploadData);
|
req.files.add(assetRawUploadData);
|
||||||
|
|
||||||
var res = await req.send(cancellationToken: cancelToken);
|
setCurrentUploadAssetCb(
|
||||||
|
CurrentUploadAsset(
|
||||||
|
id: entity.id,
|
||||||
|
createdAt: entity.createDateTime,
|
||||||
|
fileName: originalFileName,
|
||||||
|
fileType: _getAssetType(entity.type),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (res.statusCode == 201) {
|
var response = await req.send(cancellationToken: cancelToken);
|
||||||
|
|
||||||
|
if (response.statusCode == 201) {
|
||||||
singleAssetDoneCb(entity.id, deviceId);
|
singleAssetDoneCb(entity.id, deviceId);
|
||||||
|
} else {
|
||||||
|
var data = await response.stream.bytesToString();
|
||||||
|
var error = jsonDecode(data);
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
"Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}");
|
||||||
|
|
||||||
|
errorCb(ErrorUploadAsset(
|
||||||
|
asset: entity,
|
||||||
|
id: entity.id,
|
||||||
|
createdAt: entity.createDateTime,
|
||||||
|
fileName: originalFileName,
|
||||||
|
fileType: _getAssetType(entity.type),
|
||||||
|
errorMessage: error['error'],
|
||||||
|
));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on http.CancelledException {
|
} on http.CancelledException {
|
||||||
@ -108,6 +160,8 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void sendBackupRequest(AssetEntity entity) {}
|
||||||
|
|
||||||
String _getAssetType(AssetType assetType) {
|
String _getAssetType(AssetType assetType) {
|
||||||
switch (assetType) {
|
switch (assetType) {
|
||||||
case AssetType.audio:
|
case AssetType.audio:
|
||||||
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
@ -9,6 +10,7 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
|
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
|
|
||||||
class BackupControllerPage extends HookConsumerWidget {
|
class BackupControllerPage extends HookConsumerWidget {
|
||||||
@ -42,7 +44,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Server Storage",
|
"Server storage",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
@ -56,7 +58,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
barRadius: const Radius.circular(2),
|
barRadius: const Radius.circular(2),
|
||||||
lineHeight: 6.0,
|
lineHeight: 10.0,
|
||||||
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
||||||
backgroundColor: Colors.grey,
|
backgroundColor: Colors.grey,
|
||||||
progressColor: Theme.of(context).primaryColor,
|
progressColor: Theme.of(context).primaryColor,
|
||||||
@ -246,6 +248,141 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildCurrentBackupAssetInfoCard() {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.info_outline_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Uploading file info",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
|
),
|
||||||
|
if (ref.watch(errorBackupListProvider).isNotEmpty)
|
||||||
|
ActionChip(
|
||||||
|
avatar: Icon(
|
||||||
|
Icons.info,
|
||||||
|
size: 24,
|
||||||
|
color: Colors.red[400],
|
||||||
|
),
|
||||||
|
elevation: 1,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
label: Text(
|
||||||
|
"Failed (${ref.watch(errorBackupListProvider).length})",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red[400],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).push(const FailedBackupStatusRoute());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: LinearPercentIndicator(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
|
barRadius: const Radius.circular(2),
|
||||||
|
lineHeight: 10.0,
|
||||||
|
trailing: Text(
|
||||||
|
" ${backupState.progressInPercentage.toStringAsFixed(0)}%",
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
percent: backupState.progressInPercentage / 100.0,
|
||||||
|
backgroundColor: Colors.grey,
|
||||||
|
progressColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Table(
|
||||||
|
border: TableBorder.all(
|
||||||
|
color: Colors.black12,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text(
|
||||||
|
'File name: ${backupState.currentUploadAsset.fileName} [${backupState.currentUploadAsset.fileType.toLowerCase()}]',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 10.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text(
|
||||||
|
"Created on: ${DateFormat.yMMMMd('en_US').format(
|
||||||
|
DateTime.parse(
|
||||||
|
backupState.currentUploadAsset.createdAt
|
||||||
|
.toString(),
|
||||||
|
),
|
||||||
|
)}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 10.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text(
|
||||||
|
"ID: ${backupState.currentUploadAsset.id}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 10.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startBackup() {
|
||||||
|
ref.watch(errorBackupListProvider.notifier).empty();
|
||||||
|
ref.watch(backupProvider.notifier).startBackupProcess();
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@ -264,7 +401,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -297,23 +434,11 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
_buildStorageInformation(),
|
_buildStorageInformation(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
_buildCurrentBackupAssetInfoCard(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.only(
|
||||||
child: Text(
|
top: 24,
|
||||||
"Asset that were being backup: ${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length} [${backupState.progressInPercentage.toStringAsFixed(0)}%]"),
|
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
|
||||||
child: Row(children: [
|
|
||||||
const Text("Backup Progress:"),
|
|
||||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 2)),
|
|
||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
|
||||||
? const CircularProgressIndicator.adaptive()
|
|
||||||
: const Text("Done"),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
child:
|
child:
|
||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
@ -321,25 +446,33 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: Colors.red[300],
|
primary: Colors.red[300],
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
},
|
},
|
||||||
child: const Text("Cancel"),
|
child: const Text(
|
||||||
|
"CANCEL",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: Theme.of(context).primaryColor,
|
primary: Theme.of(context).primaryColor,
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
),
|
||||||
|
onPressed: shouldBackup ? startBackup : null,
|
||||||
|
child: const Text(
|
||||||
|
"START BACKUP",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: shouldBackup
|
|
||||||
? () {
|
|
||||||
ref
|
|
||||||
.read(backupProvider.notifier)
|
|
||||||
.startBackupProcess();
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: const Text("Start Backup"),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
139
mobile/lib/modules/backup/views/failed_backup_status_page.dart
Normal file
139
mobile/lib/modules/backup/views/failed_backup_status_page.dart
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class FailedBackupStatusPage extends HookConsumerWidget {
|
||||||
|
const FailedBackupStatusPage({Key? key}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final errorBackupList = ref.watch(errorBackupListProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
title: Text(
|
||||||
|
"Failed Backup (${errorBackupList.length})",
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).pop(true);
|
||||||
|
},
|
||||||
|
splashRadius: 24,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_ios_rounded,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: errorBackupList.length,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
var errorAsset = errorBackupList.elementAt(index);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12.0,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.black12,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 100,
|
||||||
|
minHeight: 150,
|
||||||
|
maxWidth: 100,
|
||||||
|
maxHeight: 200,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(15),
|
||||||
|
topLeft: Radius.circular(15),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: Image(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: AssetEntityImageProvider(
|
||||||
|
errorAsset.asset,
|
||||||
|
isOriginal: false,
|
||||||
|
thumbnailSize: const ThumbnailSize.square(512),
|
||||||
|
thumbnailFormat: ThumbnailFormat.jpeg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat.yMMMMd('en_US').format(
|
||||||
|
DateTime.parse(
|
||||||
|
errorAsset.createdAt.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: Colors.red.withAlpha(200),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Text(
|
||||||
|
errorAsset.fileName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
errorAsset.errorMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
|
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
|
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart';
|
||||||
import 'package:immich_mobile/modules/login/views/change_password_page.dart';
|
import 'package:immich_mobile/modules/login/views/change_password_page.dart';
|
||||||
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
||||||
import 'package:immich_mobile/modules/home/views/home_page.dart';
|
import 'package:immich_mobile/modules/home/views/home_page.dart';
|
||||||
@ -65,6 +66,11 @@ part 'router.gr.dart';
|
|||||||
),
|
),
|
||||||
AutoRoute(page: BackupAlbumSelectionPage, guards: [AuthGuard]),
|
AutoRoute(page: BackupAlbumSelectionPage, guards: [AuthGuard]),
|
||||||
AutoRoute(page: AlbumPreviewPage, guards: [AuthGuard]),
|
AutoRoute(page: AlbumPreviewPage, guards: [AuthGuard]),
|
||||||
|
CustomRoute(
|
||||||
|
page: FailedBackupStatusPage,
|
||||||
|
guards: [AuthGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
|
@ -115,6 +115,14 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: AlbumPreviewPage(key: args.key, album: args.album));
|
child: AlbumPreviewPage(key: args.key, album: args.album));
|
||||||
},
|
},
|
||||||
|
FailedBackupStatusRoute.name: (routeData) {
|
||||||
|
return CustomPage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const FailedBackupStatusPage(),
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
opaque: true,
|
||||||
|
barrierDismissible: false);
|
||||||
|
},
|
||||||
HomeRoute.name: (routeData) {
|
HomeRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData, child: const HomePage());
|
routeData: routeData, child: const HomePage());
|
||||||
@ -177,7 +185,9 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
RouteConfig(BackupAlbumSelectionRoute.name,
|
RouteConfig(BackupAlbumSelectionRoute.name,
|
||||||
path: '/backup-album-selection-page', guards: [authGuard]),
|
path: '/backup-album-selection-page', guards: [authGuard]),
|
||||||
RouteConfig(AlbumPreviewRoute.name,
|
RouteConfig(AlbumPreviewRoute.name,
|
||||||
path: '/album-preview-page', guards: [authGuard])
|
path: '/album-preview-page', guards: [authGuard]),
|
||||||
|
RouteConfig(FailedBackupStatusRoute.name,
|
||||||
|
path: '/failed-backup-status-page', guards: [authGuard])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,6 +447,15 @@ class AlbumPreviewRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [FailedBackupStatusPage]
|
||||||
|
class FailedBackupStatusRoute extends PageRouteInfo<void> {
|
||||||
|
const FailedBackupStatusRoute()
|
||||||
|
: super(FailedBackupStatusRoute.name, path: '/failed-backup-status-page');
|
||||||
|
|
||||||
|
static const String name = 'FailedBackupStatusRoute';
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [HomePage]
|
/// [HomePage]
|
||||||
class HomeRoute extends PageRouteInfo<void> {
|
class HomeRoute extends PageRouteInfo<void> {
|
||||||
|
@ -79,7 +79,7 @@ class NetworkService {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
} on DioError catch (e) {
|
} on DioError catch (e) {
|
||||||
debugPrint("DioError: ${e.response}");
|
debugPrint("[postRequest] DioError: ${e.response}");
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("ERROR PostRequest: $e");
|
debugPrint("ERROR PostRequest: $e");
|
||||||
|
@ -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.16.1+24
|
version: 1.17.0+25
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Logger,
|
Logger,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
@ -34,6 +35,7 @@ import { Queue } from 'bull';
|
|||||||
import { IAssetUploadedJob } from '@app/job/index';
|
import { IAssetUploadedJob } from '@app/job/index';
|
||||||
import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
|
import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
|
||||||
import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant';
|
import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant';
|
||||||
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('asset')
|
@Controller('asset')
|
||||||
@ -66,17 +68,16 @@ export class AssetController {
|
|||||||
try {
|
try {
|
||||||
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
|
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
|
||||||
|
|
||||||
if (!savedAsset) {
|
if (savedAsset) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.assetUploadedQueue.add(
|
await this.assetUploadedQueue.add(
|
||||||
assetUploadedProcessorName,
|
assetUploadedProcessorName,
|
||||||
{ asset: savedAsset, fileName: file.originalname, fileSize: file.size },
|
{ asset: savedAsset, fileName: file.originalname, fileSize: file.size },
|
||||||
{ jobId: savedAsset.id },
|
{ jobId: savedAsset.id },
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(`Error receiving upload file ${e}`);
|
Logger.error(`Error uploading file ${e}`);
|
||||||
|
throw new BadRequestException(`Error uploading file`, `${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,9 +173,9 @@ export class AssetController {
|
|||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
async checkDuplicateAsset(
|
async checkDuplicateAsset(
|
||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
@Body(ValidationPipe) { deviceAssetId }: { deviceAssetId: string },
|
@Body(ValidationPipe) checkDuplicateAssetDto: CheckDuplicateAssetDto,
|
||||||
) {
|
) {
|
||||||
const res = await this.assetService.checkDuplicatedAsset(authUser, deviceAssetId);
|
const res = await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isExist: res,
|
isExist: res,
|
||||||
|
@ -18,6 +18,7 @@ import { promisify } from 'util';
|
|||||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||||
import { SearchAssetDto } from './dto/search-asset.dto';
|
import { SearchAssetDto } from './dto/search-asset.dto';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
@ -58,15 +59,11 @@ export class AssetService {
|
|||||||
asset.mimeType = mimeType;
|
asset.mimeType = mimeType;
|
||||||
asset.duration = assetInfo.duration || null;
|
asset.duration = assetInfo.duration || null;
|
||||||
|
|
||||||
try {
|
|
||||||
const createdAsset = await this.assetRepository.save(asset);
|
const createdAsset = await this.assetRepository.save(asset);
|
||||||
if (!createdAsset) {
|
if (!createdAsset) {
|
||||||
throw new Error('Asset not created');
|
throw new Error('Asset not created');
|
||||||
}
|
}
|
||||||
return createdAsset;
|
return createdAsset;
|
||||||
} catch (e) {
|
|
||||||
Logger.error(`Error Create New Asset ${e}`, 'createUserAsset');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
|
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
|
||||||
@ -439,10 +436,11 @@ export class AssetService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkDuplicatedAsset(authUser: AuthUserDto, deviceAssetId: string) {
|
async checkDuplicatedAsset(authUser: AuthUserDto, checkDuplicateAssetDto: CheckDuplicateAssetDto) {
|
||||||
const res = await this.assetRepository.findOne({
|
const res = await this.assetRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
deviceAssetId,
|
deviceAssetId: checkDuplicateAssetDto.deviceAssetId,
|
||||||
|
deviceId: checkDuplicateAssetDto.deviceId,
|
||||||
userId: authUser.id,
|
userId: authUser.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class CheckDuplicateAssetDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
deviceAssetId!: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
deviceId!: string;
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
export const serverVersion = {
|
export const serverVersion = {
|
||||||
major: 1,
|
major: 1,
|
||||||
minor: 16,
|
minor: 17,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
build: 23,
|
build: 25,
|
||||||
};
|
};
|
||||||
|
@ -53,7 +53,7 @@ export async function fileUploader(asset: File, accessToken: string) {
|
|||||||
// Check if asset upload on server before performing upload
|
// Check if asset upload on server before performing upload
|
||||||
const res = await fetch(serverEndpoint + '/asset/check', {
|
const res = await fetch(serverEndpoint + '/asset/check', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ deviceAssetId }),
|
body: JSON.stringify({ deviceAssetId, deviceId: 'WEB' }),
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer ' + accessToken,
|
Authorization: 'Bearer ' + accessToken,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user