1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

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:
shalong-tanwen 2023-08-12 21:02:58 +00:00 committed by GitHub
parent b790354f9a
commit 77a5820c3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 318 additions and 201 deletions

View File

@ -92,6 +92,11 @@
"backup_controller_page_uploading_file_info": "Uploading file info", "backup_controller_page_uploading_file_info": "Uploading file info",
"backup_err_only_album": "Cannot remove the only album", "backup_err_only_album": "Cannot remove the only album",
"backup_info_card_assets": "assets", "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_album_thumbnails": "Library page thumbnails ({} assets)",
"cache_settings_clear_cache_button": "Clear cache", "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.", "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_cancel": "Cancel",
"delete_dialog_ok": "Delete", "delete_dialog_ok": "Delete",
"delete_dialog_title": "Delete Permanently", "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_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details", "description_input_submit_error": "Error updating description, check the log for more details",
"exif_bottom_sheet_description": "Add Description...", "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_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_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping", "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_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping", "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).", "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_save_login": "Stay logged in",
"login_form_server_empty": "Enter a server URL.", "login_form_server_empty": "Enter a server URL.",
"login_form_server_error": "Could not connect to server.", "login_form_server_error": "Could not connect to server.",
"login_disabled": "Login has been disabled",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos", "motion_photos_page_title": "Motion Photos",
"notification_permission_dialog_cancel": "Cancel", "notification_permission_dialog_cancel": "Cancel",

View File

@ -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/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/models/backup_album.model.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/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/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart';
import 'package:immich_mobile/shared/models/album.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/store.dart';
import 'package:immich_mobile/shared/models/user.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/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/db.provider.dart';
import 'package:immich_mobile/shared/providers/release_info.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/immich_logger.service.dart';
import 'package:immich_mobile/shared/services/local_notification.service.dart'; import 'package:immich_mobile/shared/services/local_notification.service.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.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:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -133,54 +122,22 @@ class ImmichAppState extends ConsumerState<ImmichApp>
switch (state) { switch (state) {
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
debugPrint("[APP STATE] resumed"); debugPrint("[APP STATE] resumed");
ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed; ref.read(appStateProvider.notifier).handleAppResume();
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);
break; break;
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
debugPrint("[APP STATE] inactive"); debugPrint("[APP STATE] inactive");
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive; ref.read(appStateProvider.notifier).handleAppInactivity();
ImmichLogger().flush();
ref.watch(websocketProvider.notifier).disconnect();
ref.watch(manualUploadProvider.notifier).cancelBackup();
ref.read(backupProvider.notifier).cancelBackup();
break; break;
case AppLifecycleState.paused: case AppLifecycleState.paused:
debugPrint("[APP STATE] paused"); debugPrint("[APP STATE] paused");
ref.watch(appStateProvider.notifier).state = AppStateEnum.paused; ref.read(appStateProvider.notifier).handleAppPause();
break; break;
case AppLifecycleState.detached: case AppLifecycleState.detached:
debugPrint("[APP STATE] detached"); debugPrint("[APP STATE] detached");
ref.watch(appStateProvider.notifier).state = AppStateEnum.detached; ref.read(appStateProvider.notifier).handleAppDetached();
break; break;
} }
} }

View File

@ -4,46 +4,51 @@ import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.d
class ManualUploadState { class ManualUploadState {
final CancellationToken cancelToken; final CancellationToken cancelToken;
final double progressInPercentage;
// Current Backup Asset // Current Backup Asset
final CurrentUploadAsset currentUploadAsset; final CurrentUploadAsset currentUploadAsset;
final int currentAssetIndex;
/// Manual Upload final bool showDetailedNotification;
final int manualUploadsTotal;
final int manualUploadFailures; /// Manual Upload Stats
final int manualUploadSuccess; final int totalAssetsToUpload;
final int successfulUploads;
final double progressInPercentage;
const ManualUploadState({ const ManualUploadState({
required this.progressInPercentage, required this.progressInPercentage,
required this.cancelToken, required this.cancelToken,
required this.currentUploadAsset, required this.currentUploadAsset,
required this.manualUploadsTotal, required this.totalAssetsToUpload,
required this.manualUploadFailures, required this.currentAssetIndex,
required this.manualUploadSuccess, required this.successfulUploads,
required this.showDetailedNotification,
}); });
ManualUploadState copyWith({ ManualUploadState copyWith({
double? progressInPercentage, double? progressInPercentage,
CancellationToken? cancelToken, CancellationToken? cancelToken,
CurrentUploadAsset? currentUploadAsset, CurrentUploadAsset? currentUploadAsset,
int? manualUploadsTotal, int? totalAssetsToUpload,
int? manualUploadFailures, int? successfulUploads,
int? manualUploadSuccess, int? currentAssetIndex,
bool? showDetailedNotification,
}) { }) {
return ManualUploadState( return ManualUploadState(
progressInPercentage: progressInPercentage ?? this.progressInPercentage, progressInPercentage: progressInPercentage ?? this.progressInPercentage,
cancelToken: cancelToken ?? this.cancelToken, cancelToken: cancelToken ?? this.cancelToken,
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
manualUploadsTotal: manualUploadsTotal ?? this.manualUploadsTotal, totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload,
manualUploadFailures: manualUploadFailures ?? this.manualUploadFailures, currentAssetIndex: currentAssetIndex ?? this.currentAssetIndex,
manualUploadSuccess: manualUploadSuccess ?? this.manualUploadSuccess, successfulUploads: successfulUploads ?? this.successfulUploads,
showDetailedNotification:
showDetailedNotification ?? this.showDetailedNotification,
); );
} }
@override @override
String toString() { 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 @override
@ -54,9 +59,10 @@ class ManualUploadState {
other.progressInPercentage == progressInPercentage && other.progressInPercentage == progressInPercentage &&
other.cancelToken == cancelToken && other.cancelToken == cancelToken &&
other.currentUploadAsset == currentUploadAsset && other.currentUploadAsset == currentUploadAsset &&
other.manualUploadsTotal == manualUploadsTotal && other.totalAssetsToUpload == totalAssetsToUpload &&
other.manualUploadFailures == manualUploadFailures && other.currentAssetIndex == currentAssetIndex &&
other.manualUploadSuccess == manualUploadSuccess; other.successfulUploads == successfulUploads &&
other.showDetailedNotification == showDetailedNotification;
} }
@override @override
@ -64,8 +70,9 @@ class ManualUploadState {
return progressInPercentage.hashCode ^ return progressInPercentage.hashCode ^
cancelToken.hashCode ^ cancelToken.hashCode ^
currentUploadAsset.hashCode ^ currentUploadAsset.hashCode ^
manualUploadsTotal.hashCode ^ totalAssetsToUpload.hashCode ^
manualUploadFailures.hashCode ^ currentAssetIndex.hashCode ^
manualUploadSuccess.hashCode; successfulUploads.hashCode ^
showDetailedNotification.hashCode;
} }
} }

View File

@ -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/error_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/manual_upload_state.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/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/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.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/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: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/services/local_notification.service.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
@ -24,24 +27,19 @@ final manualUploadProvider =
StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) { StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
return ManualUploadNotifier( return ManualUploadNotifier(
ref.watch(localNotificationService), ref.watch(localNotificationService),
ref.watch(backgroundServiceProvider),
ref.watch(backupServiceProvider),
ref.watch(backupProvider.notifier), ref.watch(backupProvider.notifier),
ref, ref,
); );
}); });
class ManualUploadNotifier extends StateNotifier<ManualUploadState> { class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
final Logger _log = Logger("ManualUploadNotifier");
final LocalNotificationService _localNotificationService; final LocalNotificationService _localNotificationService;
final BackgroundService _backgroundService;
final BackupService _backupService;
final BackupNotifier _backupProvider; final BackupNotifier _backupProvider;
final Ref ref; final Ref ref;
ManualUploadNotifier( ManualUploadNotifier(
this._localNotificationService, this._localNotificationService,
this._backgroundService,
this._backupService,
this._backupProvider, this._backupProvider,
this.ref, this.ref,
) : super( ) : super(
@ -54,15 +52,13 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
fileName: '...', fileName: '...',
fileType: '...', fileType: '...',
), ),
manualUploadsTotal: 0, totalAssetsToUpload: 0,
manualUploadSuccess: 0, successfulUploads: 0,
manualUploadFailures: 0, currentAssetIndex: 0,
showDetailedNotification: false,
), ),
); );
int get _uploadedAssetsCount =>
state.manualUploadSuccess + state.manualUploadFailures;
String _lastPrintedDetailContent = ''; String _lastPrintedDetailContent = '';
String? _lastPrintedDetailTitle; String? _lastPrintedDetailTitle;
@ -78,11 +74,12 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
_localNotificationService.showOrUpdateManualUploadStatus( _localNotificationService.showOrUpdateManualUploadStatus(
"backup_background_service_in_progress_notification".tr(), "backup_background_service_in_progress_notification".tr(),
formatAssetBackupProgress( formatAssetBackupProgress(
_uploadedAssetsCount, state.currentAssetIndex,
state.manualUploadsTotal, state.totalAssetsToUpload,
), ),
maxProgress: state.manualUploadsTotal, maxProgress: state.totalAssetsToUpload,
progress: _uploadedAssetsCount, progress: state.currentAssetIndex,
showActions: true,
); );
} }
} }
@ -103,44 +100,52 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
progress: total > 0 ? (progress * 1000) ~/ total : 0, progress: total > 0 ? (progress * 1000) ~/ total : 0,
maxProgress: 1000, maxProgress: 1000,
isDetailed: true, 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 deviceAssetId,
String deviceId, String deviceId,
bool isDuplicated, bool isDuplicated,
) { ) {
state = state.copyWith(manualUploadSuccess: state.manualUploadSuccess + 1); state = state.copyWith(successfulUploads: state.successfulUploads + 1);
_backupProvider.updateServerInfo(); _backupProvider.updateServerInfo();
if (state.manualUploadsTotal > 1) {
_throttledNotifiy();
}
} }
void _onManualBackupError(ErrorUploadAsset errorAssetInfo) { void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) {
state = ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
state.copyWith(manualUploadFailures: state.manualUploadFailures + 1);
if (state.manualUploadsTotal > 1) {
_throttledNotifiy();
}
} }
void _onProgress(int sent, int total) { void _onProgress(int sent, int total) {
final title = "backup_background_service_current_upload_notification" state = state.copyWith(
.tr(args: [state.currentUploadAsset.fileName]); progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
_throttledDetailNotify(title: title, progress: sent, total: total); );
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) { void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
state = state.copyWith(currentUploadAsset: currentUploadAsset); state = state.copyWith(
_throttledDetailNotify.title = currentUploadAsset: currentUploadAsset,
"backup_background_service_current_upload_notification" currentAssetIndex: state.currentAssetIndex + 1,
.tr(args: [currentUploadAsset.fileName]); );
_throttledDetailNotify.progress = 0; if (state.totalAssetsToUpload > 1) {
_throttledDetailNotify.total = 0; _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 { Future<bool> _startUpload(Iterable<Asset> allManualUploads) async {
@ -161,11 +166,11 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
return false; return false;
} }
// Reset state
state = state.copyWith( state = state.copyWith(
manualUploadsTotal: allManualUploads.length, progressInPercentage: 0,
manualUploadSuccess: 0, totalAssetsToUpload: allUploadAssets.length,
manualUploadFailures: 0, successfulUploads: 0,
currentAssetIndex: 0,
currentUploadAsset: CurrentUploadAsset( currentUploadAsset: CurrentUploadAsset(
id: '...', id: '...',
fileCreatedAt: DateTime.parse('2020-10-04'), fileCreatedAt: DateTime.parse('2020-10-04'),
@ -174,8 +179,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
), ),
cancelToken: CancellationToken(), cancelToken: CancellationToken(),
); );
// Reset Error List
ref.watch(errorBackupListProvider.notifier).empty();
if (state.manualUploadsTotal > 1) { if (state.totalAssetsToUpload > 1) {
_throttledNotifiy(); _throttledNotifiy();
} }
@ -184,25 +191,38 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
ref.read(appSettingsServiceProvider).getSetting<bool>( ref.read(appSettingsServiceProvider).getSetting<bool>(
AppSettingsEnum.backgroundBackupSingleProgress, AppSettingsEnum.backgroundBackupSingleProgress,
) || ) ||
state.manualUploadsTotal == 1; state.totalAssetsToUpload == 1;
state =
state.copyWith(showDetailedNotification: showDetailedNotification);
final bool ok = await _backupService.backupAsset( final bool ok = await ref.read(backupServiceProvider).backupAsset(
allUploadAssets, allUploadAssets,
state.cancelToken, state.cancelToken,
_onManualAssetUploaded, _onAssetUploaded,
showDetailedNotification ? _onProgress : (sent, total) {}, _onProgress,
showDetailedNotification ? _onSetCurrentBackupAsset : (asset) {}, _onSetCurrentBackupAsset,
_onManualBackupError, _onAssetUploadError,
); );
// Close detailed notification // Close detailed notification
await _localNotificationService.closeNotification( await _localNotificationService.closeNotification(
LocalNotificationService.manualUploadDetailedNotificationID, LocalNotificationService.manualUploadDetailedNotificationID,
); );
_log.info(
'[_startUpload] Manual Upload Completed - success: ${state.successfulUploads},'
' failed: ${state.totalAssetsToUpload - state.successfulUploads}',
);
bool hasErrors = false; bool hasErrors = false;
if ((state.manualUploadFailures != 0 && // User cancelled upload
state.manualUploadSuccess == 0) || 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)) { (!ok && !state.cancelToken.isCancelled)) {
await _localNotificationService.showOrUpdateManualUploadStatus( await _localNotificationService.showOrUpdateManualUploadStatus(
"backup_manual_title".tr(), "backup_manual_title".tr(),
@ -210,7 +230,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
presentBanner: true, presentBanner: true,
); );
hasErrors = true; hasErrors = true;
} else if (state.manualUploadSuccess != 0) { } else {
await _localNotificationService.showOrUpdateManualUploadStatus( await _localNotificationService.showOrUpdateManualUploadStatus(
"backup_manual_title".tr(), "backup_manual_title".tr(),
"backup_manual_success".tr(), "backup_manual_success".tr(),
@ -219,6 +239,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
} }
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle); _backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
_handleAppInActivity();
await _backupProvider.notifyBackgroundServiceCanRun(); await _backupProvider.notifyBackgroundServiceCanRun();
return !hasErrors; return !hasErrors;
} else { } else {
@ -228,20 +249,34 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
} catch (e) { } catch (e) {
debugPrint("ERROR _startUpload: ${e.toString()}"); debugPrint("ERROR _startUpload: ${e.toString()}");
} }
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
_handleAppInActivity();
await _localNotificationService.closeNotification( await _localNotificationService.closeNotification(
LocalNotificationService.manualUploadDetailedNotificationID, LocalNotificationService.manualUploadDetailedNotificationID,
); );
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
await _backupProvider.notifyBackgroundServiceCanRun(); await _backupProvider.notifyBackgroundServiceCanRun();
return false; 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() { void cancelBackup() {
if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { if (_backupProvider.backupProgress != BackUpProgressEnum.inProgress &&
_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
_backupProvider.notifyBackgroundServiceCanRun(); _backupProvider.notifyBackgroundServiceCanRun();
} }
state.cancelToken.cancel(); state.cancelToken.cancel();
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle); if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
}
state = state.copyWith(progressInPercentage: 0);
} }
Future<bool> uploadAssets( Future<bool> uploadAssets(
@ -250,7 +285,8 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
) async { ) async {
// assumes the background service is currently running and // assumes the background service is currently running and
// waits until it has stopped to start the backup. // 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) { if (!hasLock) {
debugPrint("[uploadAssets] could not acquire lock, exiting"); debugPrint("[uploadAssets] could not acquire lock, exiting");
ImmichToast.show( ImmichToast.show(

View File

@ -4,8 +4,10 @@ import 'package:flutter/foundation.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/models/backup_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.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/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:immich_mobile/routing/router.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
@ -13,8 +15,14 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
const CurrentUploadingAssetInfoBox({super.key}); const CurrentUploadingAssetInfoBox({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var asset = ref.watch(backupProvider).currentUploadAsset; var isManualUpload = ref.watch(backupProvider).backupProgress ==
var uploadProgress = ref.watch(backupProvider).progressInPercentage; 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); final isShowThumbnail = useState(false);
String getAssetCreationDate() { String getAssetCreationDate() {

View File

@ -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/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.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/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/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/current_backup_asset_info_box.dart';
import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart'; import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
@ -657,7 +658,9 @@ class BackupControllerPage extends HookConsumerWidget {
top: 24, top: 24,
), ),
child: Container( child: Container(
child: backupState.backupProgress == BackUpProgressEnum.inProgress child: backupState.backupProgress == BackUpProgressEnum.inProgress ||
backupState.backupProgress ==
BackUpProgressEnum.manualInProgress
? ElevatedButton( ? ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
foregroundColor: Colors.grey[50], foregroundColor: Colors.grey[50],
@ -665,7 +668,12 @@ class BackupControllerPage extends HookConsumerWidget {
// padding: const EdgeInsets.all(14), // padding: const EdgeInsets.all(14),
), ),
onPressed: () { 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( child: const Text(
"backup_controller_page_cancel", "backup_controller_page_cancel",

View File

@ -1,4 +1,19 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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 { enum AppStateEnum {
active, active,
@ -8,6 +23,70 @@ enum AppStateEnum {
detached, detached,
} }
final appStateProvider = StateProvider<AppStateEnum>((ref) { class AppStateNotiifer extends StateNotifier<AppStateEnum> {
return AppStateEnum.active; 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);
}); });

View File

@ -1,13 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hooks_riverpod/hooks_riverpod.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 { class LocalNotificationService {
static final LocalNotificationService _instance =
LocalNotificationService._internal();
final FlutterLocalNotificationsPlugin _localNotificationPlugin = final FlutterLocalNotificationsPlugin _localNotificationPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
final PermissionStatus _permissionStatus;
final Ref ref;
LocalNotificationService(this._permissionStatus, this.ref);
static const manualUploadNotificationID = 4; static const manualUploadNotificationID = 4;
static const manualUploadDetailedNotificationID = 5; static const manualUploadDetailedNotificationID = 5;
@ -15,9 +26,7 @@ class LocalNotificationService {
static const manualUploadChannelID = 'immich/manualUpload'; static const manualUploadChannelID = 'immich/manualUpload';
static const manualUploadChannelNameDetailed = 'Manual Asset Upload Detailed'; static const manualUploadChannelNameDetailed = 'Manual Asset Upload Detailed';
static const manualUploadDetailedChannelID = 'immich/manualUploadDetailed'; static const manualUploadDetailedChannelID = 'immich/manualUploadDetailed';
static const cancelUploadActionID = 'cancel_upload';
factory LocalNotificationService() => _instance;
LocalNotificationService._internal();
Future<void> setup() async { Future<void> setup() async {
const androidSetting = AndroidInitializationSettings('notification_icon'); const androidSetting = AndroidInitializationSettings('notification_icon');
@ -26,56 +35,28 @@ class LocalNotificationService {
const initSettings = const initSettings =
InitializationSettings(android: androidSetting, iOS: iosSetting); InitializationSettings(android: androidSetting, iOS: iosSetting);
await _localNotificationPlugin.initialize(initSettings); await _localNotificationPlugin.initialize(
initSettings,
onDidReceiveNotificationResponse:
_onDidReceiveForegroundNotificationResponse,
);
} }
Future<void> _showOrUpdateNotification( Future<void> _showOrUpdateNotification(
int id, int id,
String channelId,
String channelName,
String title, String title,
String body, { String body,
bool? ongoing, AndroidNotificationDetails androidNotificationDetails,
bool? playSound, DarwinNotificationDetails iosNotificationDetails,
bool? showProgress, ) async {
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,
);
final notificationDetails = NotificationDetails( final notificationDetails = NotificationDetails(
android: androidNotificationDetails, android: androidNotificationDetails,
iOS: iosNotificationDetails, 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) { Future<void> closeNotification(int id) {
@ -87,46 +68,76 @@ class LocalNotificationService {
String body, { String body, {
bool? isDetailed, bool? isDetailed,
bool? presentBanner, bool? presentBanner,
bool? showActions,
int? maxProgress, int? maxProgress,
int? progress, int? progress,
}) { }) {
var notificationlId = manualUploadNotificationID; var notificationlId = manualUploadNotificationID;
var channelId = manualUploadChannelID; var androidChannelID = manualUploadChannelID;
var channelName = manualUploadChannelName; var androidChannelName = manualUploadChannelName;
// Separate Notification for Info/Alerts and Progress // Separate Notification for Info/Alerts and Progress
if (isDetailed != null && isDetailed) { if (isDetailed != null && isDetailed) {
notificationlId = manualUploadDetailedNotificationID; notificationlId = manualUploadDetailedNotificationID;
channelId = manualUploadDetailedChannelID; androidChannelID = manualUploadDetailedChannelID;
channelName = manualUploadChannelNameDetailed; androidChannelName = manualUploadChannelNameDetailed;
} }
final isProgressNotification = maxProgress != null && progress != null; // Progress notification
return isProgressNotification final androidNotificationDetails = (maxProgress != null && progress != null)
? _showOrUpdateNotification( ? AndroidNotificationDetails(
notificationlId, androidChannelID,
channelId, androidChannelName,
channelName, ticker: title,
title,
body,
showProgress: true, showProgress: true,
onlyAlertOnce: true, onlyAlertOnce: true,
maxProgress: maxProgress, maxProgress: maxProgress,
progress: progress, progress: progress,
indeterminate: false, indeterminate: false,
presentList: true, playSound: false,
priority: Priority.low, priority: Priority.low,
importance: Importance.low, importance: Importance.low,
presentBadge: true,
ongoing: true, ongoing: true,
actions: (showActions ?? false)
? <AndroidNotificationAction>[
const AndroidNotificationAction(
cancelUploadActionID,
'Cancel',
showsUserInterface: true,
)
]
: null,
) )
: _showOrUpdateNotification( // Non-progress notification
notificationlId, : AndroidNotificationDetails(
channelId, androidChannelID,
channelName, androidChannelName,
title, playSound: false,
body,
presentList: true,
presentBadge: true,
presentBanner: presentBanner,
); );
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();
}
}
} }
} }