fix(mobile): manual asset upload - app state handling + cancel button (#3611)
* feat(mobile): Cancel manual asset upload * fix(mobile): re-add the missing translation keys * feat(mobile): show manual upload error in backup page * refactor: manual upload in-progress count * fix(mobile): handle app state properly during manual asset upload
This commit is contained in:
parent
b790354f9a
commit
77a5820c3c
8 changed files with 318 additions and 201 deletions
|
@ -92,6 +92,11 @@
|
|||
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||
"backup_err_only_album": "Cannot remove the only album",
|
||||
"backup_info_card_assets": "assets",
|
||||
"backup_manual_success": "Success",
|
||||
"backup_manual_failed": "Failed",
|
||||
"backup_manual_cancelled": "Cancelled",
|
||||
"backup_manual_title": "Upload status",
|
||||
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
|
||||
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
|
||||
"cache_settings_clear_cache_button": "Clear cache",
|
||||
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
|
||||
|
@ -138,6 +143,10 @@
|
|||
"delete_dialog_cancel": "Cancel",
|
||||
"delete_dialog_ok": "Delete",
|
||||
"delete_dialog_title": "Delete Permanently",
|
||||
"upload_dialog_title": "Upload Asset",
|
||||
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
||||
"upload_dialog_ok": "Upload",
|
||||
"upload_dialog_cancel": "Cancel",
|
||||
"description_input_hint_text": "Add description...",
|
||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||
"exif_bottom_sheet_description": "Add Description...",
|
||||
|
@ -153,6 +162,7 @@
|
|||
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
|
||||
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
|
||||
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
|
||||
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
||||
"home_page_building_timeline": "Building the timeline",
|
||||
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
|
||||
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
|
||||
|
@ -186,6 +196,7 @@
|
|||
"login_form_save_login": "Stay logged in",
|
||||
"login_form_server_empty": "Enter a server URL.",
|
||||
"login_form_server_error": "Could not connect to server.",
|
||||
"login_disabled": "Login has been disabled",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Motion Photos",
|
||||
"notification_permission_dialog_cancel": "Cancel",
|
||||
|
|
|
@ -11,13 +11,6 @@ import 'package:immich_mobile/constants/locales.dart';
|
|||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
|
@ -30,11 +23,8 @@ import 'package:immich_mobile/shared/models/logger_message.model.dart';
|
|||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/release_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/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/shared/services/local_notification.service.dart';
|
||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||
|
@ -44,7 +34,6 @@ import 'package:immich_mobile/utils/migration.dart';
|
|||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
@ -133,54 +122,22 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
debugPrint("[APP STATE] resumed");
|
||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed;
|
||||
|
||||
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
|
||||
final permission = ref.watch(galleryPermissionNotifier);
|
||||
|
||||
// Needs to be logged in and have gallery permissions
|
||||
if (isAuthenticated && (permission.isGranted || permission.isLimited)) {
|
||||
ref.read(backupProvider.notifier).resumeBackup();
|
||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
ref.watch(assetProvider.notifier).getAllAsset();
|
||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||
}
|
||||
|
||||
ref.watch(websocketProvider.notifier).connect();
|
||||
|
||||
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||
|
||||
ref
|
||||
.watch(notificationPermissionProvider.notifier)
|
||||
.getNotificationPermission();
|
||||
ref
|
||||
.watch(galleryPermissionNotifier.notifier)
|
||||
.getGalleryPermissionStatus();
|
||||
|
||||
ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
|
||||
|
||||
ref.invalidate(memoryFutureProvider);
|
||||
|
||||
ref.read(appStateProvider.notifier).handleAppResume();
|
||||
break;
|
||||
|
||||
case AppLifecycleState.inactive:
|
||||
debugPrint("[APP STATE] inactive");
|
||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
|
||||
ImmichLogger().flush();
|
||||
ref.watch(websocketProvider.notifier).disconnect();
|
||||
ref.watch(manualUploadProvider.notifier).cancelBackup();
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
|
||||
ref.read(appStateProvider.notifier).handleAppInactivity();
|
||||
break;
|
||||
|
||||
case AppLifecycleState.paused:
|
||||
debugPrint("[APP STATE] paused");
|
||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.paused;
|
||||
ref.read(appStateProvider.notifier).handleAppPause();
|
||||
break;
|
||||
|
||||
case AppLifecycleState.detached:
|
||||
debugPrint("[APP STATE] detached");
|
||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.detached;
|
||||
ref.read(appStateProvider.notifier).handleAppDetached();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,46 +4,51 @@ import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.d
|
|||
class ManualUploadState {
|
||||
final CancellationToken cancelToken;
|
||||
|
||||
final double progressInPercentage;
|
||||
|
||||
// Current Backup Asset
|
||||
final CurrentUploadAsset currentUploadAsset;
|
||||
final int currentAssetIndex;
|
||||
|
||||
/// Manual Upload
|
||||
final int manualUploadsTotal;
|
||||
final int manualUploadFailures;
|
||||
final int manualUploadSuccess;
|
||||
final bool showDetailedNotification;
|
||||
|
||||
/// Manual Upload Stats
|
||||
final int totalAssetsToUpload;
|
||||
final int successfulUploads;
|
||||
final double progressInPercentage;
|
||||
|
||||
const ManualUploadState({
|
||||
required this.progressInPercentage,
|
||||
required this.cancelToken,
|
||||
required this.currentUploadAsset,
|
||||
required this.manualUploadsTotal,
|
||||
required this.manualUploadFailures,
|
||||
required this.manualUploadSuccess,
|
||||
required this.totalAssetsToUpload,
|
||||
required this.currentAssetIndex,
|
||||
required this.successfulUploads,
|
||||
required this.showDetailedNotification,
|
||||
});
|
||||
|
||||
ManualUploadState copyWith({
|
||||
double? progressInPercentage,
|
||||
CancellationToken? cancelToken,
|
||||
CurrentUploadAsset? currentUploadAsset,
|
||||
int? manualUploadsTotal,
|
||||
int? manualUploadFailures,
|
||||
int? manualUploadSuccess,
|
||||
int? totalAssetsToUpload,
|
||||
int? successfulUploads,
|
||||
int? currentAssetIndex,
|
||||
bool? showDetailedNotification,
|
||||
}) {
|
||||
return ManualUploadState(
|
||||
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
|
||||
cancelToken: cancelToken ?? this.cancelToken,
|
||||
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
|
||||
manualUploadsTotal: manualUploadsTotal ?? this.manualUploadsTotal,
|
||||
manualUploadFailures: manualUploadFailures ?? this.manualUploadFailures,
|
||||
manualUploadSuccess: manualUploadSuccess ?? this.manualUploadSuccess,
|
||||
totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload,
|
||||
currentAssetIndex: currentAssetIndex ?? this.currentAssetIndex,
|
||||
successfulUploads: successfulUploads ?? this.successfulUploads,
|
||||
showDetailedNotification:
|
||||
showDetailedNotification ?? this.showDetailedNotification,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ManualUploadState(progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, currentUploadAsset: $currentUploadAsset, manualUploadsTotal: $manualUploadsTotal, manualUploadSuccess: $manualUploadSuccess, manualUploadFailures: $manualUploadFailures)';
|
||||
return 'ManualUploadState(progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, currentUploadAsset: $currentUploadAsset, totalAssetsToUpload: $totalAssetsToUpload, successfulUploads: $successfulUploads, currentAssetIndex: $currentAssetIndex, showDetailedNotification: $showDetailedNotification)';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -54,9 +59,10 @@ class ManualUploadState {
|
|||
other.progressInPercentage == progressInPercentage &&
|
||||
other.cancelToken == cancelToken &&
|
||||
other.currentUploadAsset == currentUploadAsset &&
|
||||
other.manualUploadsTotal == manualUploadsTotal &&
|
||||
other.manualUploadFailures == manualUploadFailures &&
|
||||
other.manualUploadSuccess == manualUploadSuccess;
|
||||
other.totalAssetsToUpload == totalAssetsToUpload &&
|
||||
other.currentAssetIndex == currentAssetIndex &&
|
||||
other.successfulUploads == successfulUploads &&
|
||||
other.showDetailedNotification == showDetailedNotification;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -64,8 +70,9 @@ class ManualUploadState {
|
|||
return progressInPercentage.hashCode ^
|
||||
cancelToken.hashCode ^
|
||||
currentUploadAsset.hashCode ^
|
||||
manualUploadsTotal.hashCode ^
|
||||
manualUploadFailures.hashCode ^
|
||||
manualUploadSuccess.hashCode;
|
||||
totalAssetsToUpload.hashCode ^
|
||||
currentAssetIndex.hashCode ^
|
||||
successfulUploads.hashCode ^
|
||||
showDetailedNotification.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,14 +9,17 @@ import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.d
|
|||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/manual_upload_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.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/onboarding/providers/gallery_permission.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/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/local_notification.service.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
|
@ -24,24 +27,19 @@ final manualUploadProvider =
|
|||
StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
|
||||
return ManualUploadNotifier(
|
||||
ref.watch(localNotificationService),
|
||||
ref.watch(backgroundServiceProvider),
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(backupProvider.notifier),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
final Logger _log = Logger("ManualUploadNotifier");
|
||||
final LocalNotificationService _localNotificationService;
|
||||
final BackgroundService _backgroundService;
|
||||
final BackupService _backupService;
|
||||
final BackupNotifier _backupProvider;
|
||||
final Ref ref;
|
||||
|
||||
ManualUploadNotifier(
|
||||
this._localNotificationService,
|
||||
this._backgroundService,
|
||||
this._backupService,
|
||||
this._backupProvider,
|
||||
this.ref,
|
||||
) : super(
|
||||
|
@ -54,15 +52,13 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
fileName: '...',
|
||||
fileType: '...',
|
||||
),
|
||||
manualUploadsTotal: 0,
|
||||
manualUploadSuccess: 0,
|
||||
manualUploadFailures: 0,
|
||||
totalAssetsToUpload: 0,
|
||||
successfulUploads: 0,
|
||||
currentAssetIndex: 0,
|
||||
showDetailedNotification: false,
|
||||
),
|
||||
);
|
||||
|
||||
int get _uploadedAssetsCount =>
|
||||
state.manualUploadSuccess + state.manualUploadFailures;
|
||||
|
||||
String _lastPrintedDetailContent = '';
|
||||
String? _lastPrintedDetailTitle;
|
||||
|
||||
|
@ -78,11 +74,12 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
_localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_background_service_in_progress_notification".tr(),
|
||||
formatAssetBackupProgress(
|
||||
_uploadedAssetsCount,
|
||||
state.manualUploadsTotal,
|
||||
state.currentAssetIndex,
|
||||
state.totalAssetsToUpload,
|
||||
),
|
||||
maxProgress: state.manualUploadsTotal,
|
||||
progress: _uploadedAssetsCount,
|
||||
maxProgress: state.totalAssetsToUpload,
|
||||
progress: state.currentAssetIndex,
|
||||
showActions: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -103,44 +100,52 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
progress: total > 0 ? (progress * 1000) ~/ total : 0,
|
||||
maxProgress: 1000,
|
||||
isDetailed: true,
|
||||
// Detailed noitifcation is displayed for Single asset uploads. Show actions for such case
|
||||
showActions: state.totalAssetsToUpload == 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onManualAssetUploaded(
|
||||
void _onAssetUploaded(
|
||||
String deviceAssetId,
|
||||
String deviceId,
|
||||
bool isDuplicated,
|
||||
) {
|
||||
state = state.copyWith(manualUploadSuccess: state.manualUploadSuccess + 1);
|
||||
state = state.copyWith(successfulUploads: state.successfulUploads + 1);
|
||||
_backupProvider.updateServerInfo();
|
||||
if (state.manualUploadsTotal > 1) {
|
||||
_throttledNotifiy();
|
||||
}
|
||||
}
|
||||
|
||||
void _onManualBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||
state =
|
||||
state.copyWith(manualUploadFailures: state.manualUploadFailures + 1);
|
||||
if (state.manualUploadsTotal > 1) {
|
||||
_throttledNotifiy();
|
||||
}
|
||||
void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) {
|
||||
ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
|
||||
}
|
||||
|
||||
void _onProgress(int sent, int total) {
|
||||
final title = "backup_background_service_current_upload_notification"
|
||||
.tr(args: [state.currentUploadAsset.fileName]);
|
||||
_throttledDetailNotify(title: title, progress: sent, total: total);
|
||||
state = state.copyWith(
|
||||
progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
|
||||
);
|
||||
if (state.showDetailedNotification) {
|
||||
final title = "backup_background_service_current_upload_notification"
|
||||
.tr(args: [state.currentUploadAsset.fileName]);
|
||||
_throttledDetailNotify(title: title, progress: sent, total: total);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
||||
state = state.copyWith(currentUploadAsset: currentUploadAsset);
|
||||
_throttledDetailNotify.title =
|
||||
"backup_background_service_current_upload_notification"
|
||||
.tr(args: [currentUploadAsset.fileName]);
|
||||
_throttledDetailNotify.progress = 0;
|
||||
_throttledDetailNotify.total = 0;
|
||||
state = state.copyWith(
|
||||
currentUploadAsset: currentUploadAsset,
|
||||
currentAssetIndex: state.currentAssetIndex + 1,
|
||||
);
|
||||
if (state.totalAssetsToUpload > 1) {
|
||||
_throttledNotifiy();
|
||||
}
|
||||
if (state.showDetailedNotification) {
|
||||
_throttledDetailNotify.title =
|
||||
"backup_background_service_current_upload_notification"
|
||||
.tr(args: [currentUploadAsset.fileName]);
|
||||
_throttledDetailNotify.progress = 0;
|
||||
_throttledDetailNotify.total = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _startUpload(Iterable<Asset> allManualUploads) async {
|
||||
|
@ -161,11 +166,11 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
state = state.copyWith(
|
||||
manualUploadsTotal: allManualUploads.length,
|
||||
manualUploadSuccess: 0,
|
||||
manualUploadFailures: 0,
|
||||
progressInPercentage: 0,
|
||||
totalAssetsToUpload: allUploadAssets.length,
|
||||
successfulUploads: 0,
|
||||
currentAssetIndex: 0,
|
||||
currentUploadAsset: CurrentUploadAsset(
|
||||
id: '...',
|
||||
fileCreatedAt: DateTime.parse('2020-10-04'),
|
||||
|
@ -174,8 +179,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
),
|
||||
cancelToken: CancellationToken(),
|
||||
);
|
||||
// Reset Error List
|
||||
ref.watch(errorBackupListProvider.notifier).empty();
|
||||
|
||||
if (state.manualUploadsTotal > 1) {
|
||||
if (state.totalAssetsToUpload > 1) {
|
||||
_throttledNotifiy();
|
||||
}
|
||||
|
||||
|
@ -184,25 +191,38 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
ref.read(appSettingsServiceProvider).getSetting<bool>(
|
||||
AppSettingsEnum.backgroundBackupSingleProgress,
|
||||
) ||
|
||||
state.manualUploadsTotal == 1;
|
||||
state.totalAssetsToUpload == 1;
|
||||
state =
|
||||
state.copyWith(showDetailedNotification: showDetailedNotification);
|
||||
|
||||
final bool ok = await _backupService.backupAsset(
|
||||
allUploadAssets,
|
||||
state.cancelToken,
|
||||
_onManualAssetUploaded,
|
||||
showDetailedNotification ? _onProgress : (sent, total) {},
|
||||
showDetailedNotification ? _onSetCurrentBackupAsset : (asset) {},
|
||||
_onManualBackupError,
|
||||
);
|
||||
final bool ok = await ref.read(backupServiceProvider).backupAsset(
|
||||
allUploadAssets,
|
||||
state.cancelToken,
|
||||
_onAssetUploaded,
|
||||
_onProgress,
|
||||
_onSetCurrentBackupAsset,
|
||||
_onAssetUploadError,
|
||||
);
|
||||
|
||||
// Close detailed notification
|
||||
await _localNotificationService.closeNotification(
|
||||
LocalNotificationService.manualUploadDetailedNotificationID,
|
||||
);
|
||||
|
||||
_log.info(
|
||||
'[_startUpload] Manual Upload Completed - success: ${state.successfulUploads},'
|
||||
' failed: ${state.totalAssetsToUpload - state.successfulUploads}',
|
||||
);
|
||||
bool hasErrors = false;
|
||||
if ((state.manualUploadFailures != 0 &&
|
||||
state.manualUploadSuccess == 0) ||
|
||||
// User cancelled upload
|
||||
if (!ok && state.cancelToken.isCancelled) {
|
||||
await _localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_manual_title".tr(),
|
||||
"backup_manual_cancelled".tr(),
|
||||
presentBanner: true,
|
||||
);
|
||||
hasErrors = true;
|
||||
} else if (state.successfulUploads == 0 ||
|
||||
(!ok && !state.cancelToken.isCancelled)) {
|
||||
await _localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_manual_title".tr(),
|
||||
|
@ -210,7 +230,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
presentBanner: true,
|
||||
);
|
||||
hasErrors = true;
|
||||
} else if (state.manualUploadSuccess != 0) {
|
||||
} else {
|
||||
await _localNotificationService.showOrUpdateManualUploadStatus(
|
||||
"backup_manual_title".tr(),
|
||||
"backup_manual_success".tr(),
|
||||
|
@ -219,6 +239,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
}
|
||||
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
_handleAppInActivity();
|
||||
await _backupProvider.notifyBackgroundServiceCanRun();
|
||||
return !hasErrors;
|
||||
} else {
|
||||
|
@ -228,20 +249,34 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
} catch (e) {
|
||||
debugPrint("ERROR _startUpload: ${e.toString()}");
|
||||
}
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
_handleAppInActivity();
|
||||
await _localNotificationService.closeNotification(
|
||||
LocalNotificationService.manualUploadDetailedNotificationID,
|
||||
);
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
await _backupProvider.notifyBackgroundServiceCanRun();
|
||||
return false;
|
||||
}
|
||||
|
||||
void _handleAppInActivity() {
|
||||
final appState = ref.read(appStateProvider.notifier).getAppState();
|
||||
// The app is currently in background. Perform the necessary cleanups which
|
||||
// are on-hold for upload completion
|
||||
if (appState != AppStateEnum.active || appState != AppStateEnum.resumed) {
|
||||
ref.read(appStateProvider.notifier).handleAppInactivity();
|
||||
}
|
||||
}
|
||||
|
||||
void cancelBackup() {
|
||||
if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
if (_backupProvider.backupProgress != BackUpProgressEnum.inProgress &&
|
||||
_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
_backupProvider.notifyBackgroundServiceCanRun();
|
||||
}
|
||||
state.cancelToken.cancel();
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
|
||||
}
|
||||
state = state.copyWith(progressInPercentage: 0);
|
||||
}
|
||||
|
||||
Future<bool> uploadAssets(
|
||||
|
@ -250,7 +285,8 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||
) async {
|
||||
// assumes the background service is currently running and
|
||||
// waits until it has stopped to start the backup.
|
||||
final bool hasLock = await _backgroundService.acquireLock();
|
||||
final bool hasLock =
|
||||
await ref.read(backgroundServiceProvider).acquireLock();
|
||||
if (!hasLock) {
|
||||
debugPrint("[uploadAssets] could not acquire lock, exiting");
|
||||
ImmichToast.show(
|
||||
|
|
|
@ -4,8 +4,10 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
|
@ -13,8 +15,14 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
|||
const CurrentUploadingAssetInfoBox({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var asset = ref.watch(backupProvider).currentUploadAsset;
|
||||
var uploadProgress = ref.watch(backupProvider).progressInPercentage;
|
||||
var isManualUpload = ref.watch(backupProvider).backupProgress ==
|
||||
BackUpProgressEnum.manualInProgress;
|
||||
var asset = !isManualUpload
|
||||
? ref.watch(backupProvider).currentUploadAsset
|
||||
: ref.watch(manualUploadProvider).currentUploadAsset;
|
||||
var uploadProgress = !isManualUpload
|
||||
? ref.watch(backupProvider).progressInPercentage
|
||||
: ref.watch(manualUploadProvider).progressInPercentage;
|
||||
final isShowThumbnail = useState(false);
|
||||
|
||||
String getAssetCreationDate() {
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup_verification.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
|
||||
|
@ -657,7 +658,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||
top: 24,
|
||||
),
|
||||
child: Container(
|
||||
child: backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||
child: backupState.backupProgress == BackUpProgressEnum.inProgress ||
|
||||
backupState.backupProgress ==
|
||||
BackUpProgressEnum.manualInProgress
|
||||
? ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.grey[50],
|
||||
|
@ -665,7 +668,12 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||
// padding: const EdgeInsets.all(14),
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
if (backupState.backupProgress ==
|
||||
BackUpProgressEnum.manualInProgress) {
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
} else {
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
}
|
||||
},
|
||||
child: const Text(
|
||||
"backup_controller_page_cancel",
|
||||
|
|
|
@ -1,4 +1,19 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/release_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/services/immich_logger.service.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
enum AppStateEnum {
|
||||
active,
|
||||
|
@ -8,6 +23,70 @@ enum AppStateEnum {
|
|||
detached,
|
||||
}
|
||||
|
||||
final appStateProvider = StateProvider<AppStateEnum>((ref) {
|
||||
return AppStateEnum.active;
|
||||
class AppStateNotiifer extends StateNotifier<AppStateEnum> {
|
||||
final Ref ref;
|
||||
|
||||
AppStateNotiifer(this.ref) : super(AppStateEnum.active);
|
||||
|
||||
void updateAppState(AppStateEnum appState) {
|
||||
state = appState;
|
||||
}
|
||||
|
||||
AppStateEnum getAppState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
void handleAppResume() {
|
||||
state = AppStateEnum.resumed;
|
||||
|
||||
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
|
||||
final permission = ref.watch(galleryPermissionNotifier);
|
||||
|
||||
// Needs to be logged in and have gallery permissions
|
||||
if (isAuthenticated && (permission.isGranted || permission.isLimited)) {
|
||||
ref.read(backupProvider.notifier).resumeBackup();
|
||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
ref.watch(assetProvider.notifier).getAllAsset();
|
||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||
}
|
||||
|
||||
ref.watch(websocketProvider.notifier).connect();
|
||||
|
||||
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||
|
||||
ref
|
||||
.watch(notificationPermissionProvider.notifier)
|
||||
.getNotificationPermission();
|
||||
ref.watch(galleryPermissionNotifier.notifier).getGalleryPermissionStatus();
|
||||
|
||||
ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
|
||||
|
||||
ref.invalidate(memoryFutureProvider);
|
||||
}
|
||||
|
||||
void handleAppInactivity() {
|
||||
state = AppStateEnum.inactive;
|
||||
|
||||
// Do not handle inactivity if manual upload is in progress
|
||||
if (ref.watch(backupProvider.notifier).backupProgress !=
|
||||
BackUpProgressEnum.manualInProgress) {
|
||||
ImmichLogger().flush();
|
||||
ref.read(websocketProvider.notifier).disconnect();
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
}
|
||||
}
|
||||
|
||||
void handleAppPause() {
|
||||
state = AppStateEnum.paused;
|
||||
}
|
||||
|
||||
void handleAppDetached() {
|
||||
state = AppStateEnum.detached;
|
||||
ref.watch(manualUploadProvider.notifier).cancelBackup();
|
||||
}
|
||||
}
|
||||
|
||||
final appStateProvider =
|
||||
StateNotifierProvider<AppStateNotiifer, AppStateEnum>((ref) {
|
||||
return AppStateNotiifer(ref);
|
||||
});
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
final localNotificationService = Provider((ref) => LocalNotificationService());
|
||||
final localNotificationService = Provider(
|
||||
(ref) => LocalNotificationService(
|
||||
ref.watch(notificationPermissionProvider),
|
||||
ref,
|
||||
),
|
||||
);
|
||||
|
||||
class LocalNotificationService {
|
||||
static final LocalNotificationService _instance =
|
||||
LocalNotificationService._internal();
|
||||
final FlutterLocalNotificationsPlugin _localNotificationPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
final PermissionStatus _permissionStatus;
|
||||
final Ref ref;
|
||||
|
||||
LocalNotificationService(this._permissionStatus, this.ref);
|
||||
|
||||
static const manualUploadNotificationID = 4;
|
||||
static const manualUploadDetailedNotificationID = 5;
|
||||
|
@ -15,9 +26,7 @@ class LocalNotificationService {
|
|||
static const manualUploadChannelID = 'immich/manualUpload';
|
||||
static const manualUploadChannelNameDetailed = 'Manual Asset Upload Detailed';
|
||||
static const manualUploadDetailedChannelID = 'immich/manualUploadDetailed';
|
||||
|
||||
factory LocalNotificationService() => _instance;
|
||||
LocalNotificationService._internal();
|
||||
static const cancelUploadActionID = 'cancel_upload';
|
||||
|
||||
Future<void> setup() async {
|
||||
const androidSetting = AndroidInitializationSettings('notification_icon');
|
||||
|
@ -26,56 +35,28 @@ class LocalNotificationService {
|
|||
const initSettings =
|
||||
InitializationSettings(android: androidSetting, iOS: iosSetting);
|
||||
|
||||
await _localNotificationPlugin.initialize(initSettings);
|
||||
await _localNotificationPlugin.initialize(
|
||||
initSettings,
|
||||
onDidReceiveNotificationResponse:
|
||||
_onDidReceiveForegroundNotificationResponse,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showOrUpdateNotification(
|
||||
int id,
|
||||
String channelId,
|
||||
String channelName,
|
||||
String title,
|
||||
String body, {
|
||||
bool? ongoing,
|
||||
bool? playSound,
|
||||
bool? showProgress,
|
||||
Priority? priority,
|
||||
Importance? importance,
|
||||
bool? onlyAlertOnce,
|
||||
int? maxProgress,
|
||||
int? progress,
|
||||
bool? indeterminate,
|
||||
bool? presentBadge,
|
||||
bool? presentBanner,
|
||||
bool? presentList,
|
||||
}) async {
|
||||
var androidNotificationDetails = AndroidNotificationDetails(
|
||||
channelId,
|
||||
channelName,
|
||||
ticker: title,
|
||||
playSound: playSound ?? false,
|
||||
showProgress: showProgress ?? false,
|
||||
maxProgress: maxProgress ?? 0,
|
||||
progress: progress ?? 0,
|
||||
onlyAlertOnce: onlyAlertOnce ?? false,
|
||||
indeterminate: indeterminate ?? false,
|
||||
priority: priority ?? Priority.defaultPriority,
|
||||
importance: importance ?? Importance.defaultImportance,
|
||||
ongoing: ongoing ?? false,
|
||||
);
|
||||
|
||||
var iosNotificationDetails = DarwinNotificationDetails(
|
||||
presentBadge: presentBadge ?? false,
|
||||
presentBanner: presentBanner ?? false,
|
||||
presentList: presentList ?? false,
|
||||
|
||||
);
|
||||
|
||||
String body,
|
||||
AndroidNotificationDetails androidNotificationDetails,
|
||||
DarwinNotificationDetails iosNotificationDetails,
|
||||
) async {
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
iOS: iosNotificationDetails,
|
||||
);
|
||||
|
||||
await _localNotificationPlugin.show(id, title, body, notificationDetails);
|
||||
if (_permissionStatus == PermissionStatus.granted) {
|
||||
await _localNotificationPlugin.show(id, title, body, notificationDetails);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeNotification(int id) {
|
||||
|
@ -87,46 +68,76 @@ class LocalNotificationService {
|
|||
String body, {
|
||||
bool? isDetailed,
|
||||
bool? presentBanner,
|
||||
bool? showActions,
|
||||
int? maxProgress,
|
||||
int? progress,
|
||||
}) {
|
||||
var notificationlId = manualUploadNotificationID;
|
||||
var channelId = manualUploadChannelID;
|
||||
var channelName = manualUploadChannelName;
|
||||
var androidChannelID = manualUploadChannelID;
|
||||
var androidChannelName = manualUploadChannelName;
|
||||
// Separate Notification for Info/Alerts and Progress
|
||||
if (isDetailed != null && isDetailed) {
|
||||
notificationlId = manualUploadDetailedNotificationID;
|
||||
channelId = manualUploadDetailedChannelID;
|
||||
channelName = manualUploadChannelNameDetailed;
|
||||
androidChannelID = manualUploadDetailedChannelID;
|
||||
androidChannelName = manualUploadChannelNameDetailed;
|
||||
}
|
||||
final isProgressNotification = maxProgress != null && progress != null;
|
||||
return isProgressNotification
|
||||
? _showOrUpdateNotification(
|
||||
notificationlId,
|
||||
channelId,
|
||||
channelName,
|
||||
title,
|
||||
body,
|
||||
// Progress notification
|
||||
final androidNotificationDetails = (maxProgress != null && progress != null)
|
||||
? AndroidNotificationDetails(
|
||||
androidChannelID,
|
||||
androidChannelName,
|
||||
ticker: title,
|
||||
showProgress: true,
|
||||
onlyAlertOnce: true,
|
||||
maxProgress: maxProgress,
|
||||
progress: progress,
|
||||
indeterminate: false,
|
||||
presentList: true,
|
||||
playSound: false,
|
||||
priority: Priority.low,
|
||||
importance: Importance.low,
|
||||
presentBadge: true,
|
||||
ongoing: true,
|
||||
actions: (showActions ?? false)
|
||||
? <AndroidNotificationAction>[
|
||||
const AndroidNotificationAction(
|
||||
cancelUploadActionID,
|
||||
'Cancel',
|
||||
showsUserInterface: true,
|
||||
)
|
||||
]
|
||||
: null,
|
||||
)
|
||||
: _showOrUpdateNotification(
|
||||
notificationlId,
|
||||
channelId,
|
||||
channelName,
|
||||
title,
|
||||
body,
|
||||
presentList: true,
|
||||
presentBadge: true,
|
||||
presentBanner: presentBanner,
|
||||
// Non-progress notification
|
||||
: AndroidNotificationDetails(
|
||||
androidChannelID,
|
||||
androidChannelName,
|
||||
playSound: false,
|
||||
);
|
||||
|
||||
final iosNotificationDetails = DarwinNotificationDetails(
|
||||
presentBadge: true,
|
||||
presentList: true,
|
||||
presentBanner: presentBanner,
|
||||
);
|
||||
|
||||
return _showOrUpdateNotification(
|
||||
notificationlId,
|
||||
title,
|
||||
body,
|
||||
androidNotificationDetails,
|
||||
iosNotificationDetails,
|
||||
);
|
||||
}
|
||||
|
||||
void _onDidReceiveForegroundNotificationResponse(
|
||||
NotificationResponse notificationResponse,
|
||||
) {
|
||||
// Handle notification actions
|
||||
switch (notificationResponse.actionId) {
|
||||
case cancelUploadActionID:
|
||||
{
|
||||
debugPrint("User cancelled manual upload operation");
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue