mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	show notifications on background backup errors (#496)
* show notifications on background backup errors * settings page to configure (background backup error) notifications * persist time since failed background backup * fix darkmode slider color
This commit is contained in:
		
							parent
							
								
									c436c57cc9
								
							
						
					
					
						commit
						3125d04f32
					
				@ -138,6 +138,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
				
			|||||||
                immediate = true,
 | 
					                immediate = true,
 | 
				
			||||||
                requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
 | 
					                requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
 | 
				
			||||||
                requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
 | 
					                requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
 | 
				
			||||||
 | 
					                initialDelayInMs = ONE_MINUTE,
 | 
				
			||||||
                retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
 | 
					                retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        engine?.destroy()
 | 
					        engine?.destroy()
 | 
				
			||||||
@ -169,6 +170,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
				
			|||||||
                                    immediate = true,
 | 
					                                    immediate = true,
 | 
				
			||||||
                                    requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
 | 
					                                    requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
 | 
				
			||||||
                                    requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
 | 
					                                    requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
 | 
				
			||||||
 | 
					                                    initialDelayInMs = ONE_MINUTE,
 | 
				
			||||||
                                    retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
 | 
					                                    retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
@ -186,22 +188,27 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
				
			|||||||
                val args = call.arguments<ArrayList<*>>()!!
 | 
					                val args = call.arguments<ArrayList<*>>()!!
 | 
				
			||||||
                val title = args.get(0) as String
 | 
					                val title = args.get(0) as String
 | 
				
			||||||
                val content = args.get(1) as String
 | 
					                val content = args.get(1) as String
 | 
				
			||||||
                showError(title, content)
 | 
					                val individualTag = args.get(2) as String?
 | 
				
			||||||
 | 
					                showError(title, content, individualTag)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            "clearErrorNotifications" -> clearErrorNotifications()
 | 
				
			||||||
            else -> r.notImplemented()
 | 
					            else -> r.notImplemented()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun showError(title: String, content: String) {
 | 
					    private fun showError(title: String, content: String, individualTag: String?) {
 | 
				
			||||||
        val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
 | 
					        val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
 | 
				
			||||||
           .setContentTitle(title)
 | 
					           .setContentTitle(title)
 | 
				
			||||||
           .setTicker(title)
 | 
					           .setTicker(title)
 | 
				
			||||||
           .setContentText(content)
 | 
					           .setContentText(content)
 | 
				
			||||||
           .setSmallIcon(R.mipmap.ic_launcher)
 | 
					           .setSmallIcon(R.mipmap.ic_launcher)
 | 
				
			||||||
           .setAutoCancel(true)
 | 
					           .setOnlyAlertOnce(true)
 | 
				
			||||||
           .build()
 | 
					           .build()
 | 
				
			||||||
        val notificationId = SystemClock.uptimeMillis() as Int
 | 
					        notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
 | 
				
			||||||
        notificationManager.notify(notificationId, notification)
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun clearErrorNotifications() {
 | 
				
			||||||
 | 
					        notificationManager.cancel(NOTIFICATION_ERROR_ID)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
 | 
					    private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
 | 
				
			||||||
@ -212,14 +219,14 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
				
			|||||||
           .setSmallIcon(R.mipmap.ic_launcher)
 | 
					           .setSmallIcon(R.mipmap.ic_launcher)
 | 
				
			||||||
           .setOngoing(true)
 | 
					           .setOngoing(true)
 | 
				
			||||||
           .build()
 | 
					           .build()
 | 
				
			||||||
       return ForegroundInfo(1, notification)
 | 
					       return ForegroundInfo(NOTIFICATION_ID, notification)
 | 
				
			||||||
   }
 | 
					   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @RequiresApi(Build.VERSION_CODES.O)
 | 
					    @RequiresApi(Build.VERSION_CODES.O)
 | 
				
			||||||
    private fun createChannel() {
 | 
					    private fun createChannel() {
 | 
				
			||||||
        val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
 | 
					        val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
 | 
				
			||||||
        notificationManager.createNotificationChannel(foreground)
 | 
					        notificationManager.createNotificationChannel(foreground)
 | 
				
			||||||
        val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_HIGH)
 | 
					        val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_DEFAULT)
 | 
				
			||||||
        notificationManager.createNotificationChannel(error)
 | 
					        notificationManager.createNotificationChannel(error)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -236,6 +243,9 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
				
			|||||||
        private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
 | 
					        private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
 | 
				
			||||||
        private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
 | 
					        private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
 | 
				
			||||||
        private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
 | 
					        private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
 | 
				
			||||||
 | 
					        private const val NOTIFICATION_ID = 1
 | 
				
			||||||
 | 
					        private const val NOTIFICATION_ERROR_ID = 2 
 | 
				
			||||||
 | 
					        private const val ONE_MINUTE: Long = 60000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Enqueues the `BackupWorker` to run when all constraints are met.
 | 
					         * Enqueues the `BackupWorker` to run when all constraints are met.
 | 
				
			||||||
@ -262,6 +272,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
				
			|||||||
                                    keepExisting: Boolean = false,
 | 
					                                    keepExisting: Boolean = false,
 | 
				
			||||||
                                    requireUnmeteredNetwork: Boolean = false,
 | 
					                                    requireUnmeteredNetwork: Boolean = false,
 | 
				
			||||||
                                    requireCharging: Boolean = false,
 | 
					                                    requireCharging: Boolean = false,
 | 
				
			||||||
 | 
					                                    initialDelayInMs: Long = 0,
 | 
				
			||||||
                                    retries: Int = 0) {
 | 
					                                    retries: Int = 0) {
 | 
				
			||||||
            if (!isEnabled(context)) {
 | 
					            if (!isEnabled(context)) {
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
@ -287,9 +298,10 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
				
			|||||||
            val photoCheck = OneTimeWorkRequest.Builder(BackupWorker::class.java)
 | 
					            val photoCheck = OneTimeWorkRequest.Builder(BackupWorker::class.java)
 | 
				
			||||||
                .setConstraints(constraints.build())
 | 
					                .setConstraints(constraints.build())
 | 
				
			||||||
                .setInputData(inputData)
 | 
					                .setInputData(inputData)
 | 
				
			||||||
 | 
					                .setInitialDelay(initialDelayInMs, TimeUnit.MILLISECONDS)
 | 
				
			||||||
                .setBackoffCriteria(
 | 
					                .setBackoffCriteria(
 | 
				
			||||||
                    BackoffPolicy.EXPONENTIAL,
 | 
					                    BackoffPolicy.EXPONENTIAL,
 | 
				
			||||||
                    OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
 | 
					                    ONE_MINUTE,
 | 
				
			||||||
                    TimeUnit.MILLISECONDS)
 | 
					                    TimeUnit.MILLISECONDS)
 | 
				
			||||||
                .build()
 | 
					                .build()
 | 
				
			||||||
            val policy = if (immediate) ExistingWorkPolicy.REPLACE else (if (keepExisting) ExistingWorkPolicy.KEEP else ExistingWorkPolicy.APPEND_OR_REPLACE)
 | 
					            val policy = if (immediate) ExistingWorkPolicy.REPLACE else (if (keepExisting) ExistingWorkPolicy.KEEP else ExistingWorkPolicy.APPEND_OR_REPLACE)
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,9 @@
 | 
				
			|||||||
  "backup_background_service_upload_failure_notification": "Failed to upload {}",
 | 
					  "backup_background_service_upload_failure_notification": "Failed to upload {}",
 | 
				
			||||||
  "backup_background_service_in_progress_notification": "Backing up your assets…",
 | 
					  "backup_background_service_in_progress_notification": "Backing up your assets…",
 | 
				
			||||||
  "backup_background_service_current_upload_notification": "Uploading {}",
 | 
					  "backup_background_service_current_upload_notification": "Uploading {}",
 | 
				
			||||||
 | 
					  "backup_background_service_error_title": "Backup error",
 | 
				
			||||||
 | 
					  "backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
 | 
				
			||||||
 | 
					  "backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
 | 
				
			||||||
  "backup_controller_page_albums": "Backup Albums",
 | 
					  "backup_controller_page_albums": "Backup Albums",
 | 
				
			||||||
  "backup_controller_page_backup": "Backup",
 | 
					  "backup_controller_page_backup": "Backup",
 | 
				
			||||||
  "backup_controller_page_backup_selected": "Selected: ",
 | 
					  "backup_controller_page_backup_selected": "Selected: ",
 | 
				
			||||||
@ -139,5 +142,12 @@
 | 
				
			|||||||
  "asset_list_settings_title": "Photo Grid",
 | 
					  "asset_list_settings_title": "Photo Grid",
 | 
				
			||||||
  "asset_list_settings_subtitle": "Photo grid layout settings",
 | 
					  "asset_list_settings_subtitle": "Photo grid layout settings",
 | 
				
			||||||
  "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
 | 
					  "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
 | 
				
			||||||
  "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})"
 | 
					  "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
 | 
				
			||||||
 | 
					  "setting_notifications_title": "Notifications",
 | 
				
			||||||
 | 
					  "setting_notifications_subtitle": "Adjust your notification preferences",
 | 
				
			||||||
 | 
					  "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
 | 
				
			||||||
 | 
					  "setting_notifications_notify_immediately": "immediately",
 | 
				
			||||||
 | 
					  "setting_notifications_notify_minutes": "{} minutes",
 | 
				
			||||||
 | 
					  "setting_notifications_notify_hours": "{} hours",
 | 
				
			||||||
 | 
					  "setting_notifications_notify_never": "never"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -19,3 +19,7 @@ const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// User Setting Info
 | 
					// User Setting Info
 | 
				
			||||||
const String userSettingInfoBox = "immichUserSettingInfoBox";
 | 
					const String userSettingInfoBox = "immichUserSettingInfoBox";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Background backup Info
 | 
				
			||||||
 | 
					const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
 | 
				
			||||||
 | 
					const String backupFailedSince = "immichBackupFailedSince"; // Key 1
 | 
				
			||||||
@ -4,6 +4,7 @@ import 'dart:io';
 | 
				
			|||||||
import 'dart:isolate';
 | 
					import 'dart:isolate';
 | 
				
			||||||
import 'dart:ui' show IsolateNameServer, PluginUtilities;
 | 
					import 'dart:ui' show IsolateNameServer, PluginUtilities;
 | 
				
			||||||
import 'package:cancellation_token_http/http.dart';
 | 
					import 'package:cancellation_token_http/http.dart';
 | 
				
			||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:flutter/widgets.dart';
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
@ -16,6 +17,7 @@ import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dar
 | 
				
			|||||||
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/services/backup.service.dart';
 | 
					import 'package:immich_mobile/modules/backup/services/backup.service.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
					import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/services/api.service.dart';
 | 
					import 'package:immich_mobile/shared/services/api.service.dart';
 | 
				
			||||||
import 'package:photo_manager/photo_manager.dart';
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -39,6 +41,7 @@ class BackgroundService {
 | 
				
			|||||||
  bool _hasLock = false;
 | 
					  bool _hasLock = false;
 | 
				
			||||||
  SendPort? _waitingIsolate;
 | 
					  SendPort? _waitingIsolate;
 | 
				
			||||||
  ReceivePort? _rp;
 | 
					  ReceivePort? _rp;
 | 
				
			||||||
 | 
					  bool _errorGracePeriodExceeded = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool get isForegroundInitialized {
 | 
					  bool get isForegroundInitialized {
 | 
				
			||||||
    return _isForegroundInitialized;
 | 
					    return _isForegroundInitialized;
 | 
				
			||||||
@ -140,8 +143,8 @@ class BackgroundService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Updates the notification shown by the background service
 | 
					  /// Updates the notification shown by the background service
 | 
				
			||||||
  Future<bool> updateNotification({
 | 
					  Future<bool> _updateNotification({
 | 
				
			||||||
    String title = "Immich",
 | 
					    required String title,
 | 
				
			||||||
    String? content,
 | 
					    String? content,
 | 
				
			||||||
  }) async {
 | 
					  }) async {
 | 
				
			||||||
    if (!Platform.isAndroid) {
 | 
					    if (!Platform.isAndroid) {
 | 
				
			||||||
@ -153,28 +156,44 @@ class BackgroundService {
 | 
				
			|||||||
            .invokeMethod('updateNotification', [title, content]);
 | 
					            .invokeMethod('updateNotification', [title, content]);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      debugPrint("[updateNotification] failed to communicate with plugin");
 | 
					      debugPrint("[_updateNotification] failed to communicate with plugin");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return Future.value(false);
 | 
					    return Future.value(false);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Shows a new priority notification
 | 
					  /// Shows a new priority notification
 | 
				
			||||||
  Future<bool> showErrorNotification(
 | 
					  Future<bool> _showErrorNotification({
 | 
				
			||||||
    String title,
 | 
					    required String title,
 | 
				
			||||||
    String content,
 | 
					    String? content,
 | 
				
			||||||
  ) async {
 | 
					    String? individualTag,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    if (!Platform.isAndroid) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if (_isBackgroundInitialized && _errorGracePeriodExceeded) {
 | 
				
			||||||
 | 
					        return await _backgroundChannel
 | 
				
			||||||
 | 
					            .invokeMethod('showError', [title, content, individualTag]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      debugPrint("[_showErrorNotification] failed to communicate with plugin");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<bool> _clearErrorNotifications() async {
 | 
				
			||||||
    if (!Platform.isAndroid) {
 | 
					    if (!Platform.isAndroid) {
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      if (_isBackgroundInitialized) {
 | 
					      if (_isBackgroundInitialized) {
 | 
				
			||||||
        return await _backgroundChannel
 | 
					        return await _backgroundChannel.invokeMethod('clearErrorNotifications');
 | 
				
			||||||
            .invokeMethod('showError', [title, content]);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      debugPrint("[showErrorNotification] failed to communicate with plugin");
 | 
					      debugPrint(
 | 
				
			||||||
 | 
					          "[_clearErrorNotifications] failed to communicate with plugin");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return Future.value(false);
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// await to ensure this thread (foreground or background) has exclusive access
 | 
					  /// await to ensure this thread (foreground or background) has exclusive access
 | 
				
			||||||
@ -278,7 +297,15 @@ class BackgroundService {
 | 
				
			|||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          await translationsLoaded;
 | 
					          await translationsLoaded;
 | 
				
			||||||
          return await _onAssetsChanged();
 | 
					          final bool ok = await _onAssetsChanged();
 | 
				
			||||||
 | 
					          if (ok) {
 | 
				
			||||||
 | 
					            Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
 | 
				
			||||||
 | 
					          } else if (Hive.box(backgroundBackupInfoBox).get(backupFailedSince) ==
 | 
				
			||||||
 | 
					              null) {
 | 
				
			||||||
 | 
					            Hive.box(backgroundBackupInfoBox)
 | 
				
			||||||
 | 
					                .put(backupFailedSince, DateTime.now());
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return ok;
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
          debugPrint(error.toString());
 | 
					          debugPrint(error.toString());
 | 
				
			||||||
          return false;
 | 
					          return false;
 | 
				
			||||||
@ -303,6 +330,8 @@ class BackgroundService {
 | 
				
			|||||||
    Hive.registerAdapter(HiveBackupAlbumsAdapter());
 | 
					    Hive.registerAdapter(HiveBackupAlbumsAdapter());
 | 
				
			||||||
    await Hive.openBox(userInfoBox);
 | 
					    await Hive.openBox(userInfoBox);
 | 
				
			||||||
    await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
 | 
					    await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
 | 
				
			||||||
 | 
					    await Hive.openBox(userSettingInfoBox);
 | 
				
			||||||
 | 
					    await Hive.openBox(backgroundBackupInfoBox);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ApiService apiService = ApiService();
 | 
					    ApiService apiService = ApiService();
 | 
				
			||||||
    apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
 | 
					    apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
 | 
				
			||||||
@ -313,23 +342,36 @@ class BackgroundService {
 | 
				
			|||||||
        await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
					        await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
				
			||||||
    final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
 | 
					    final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
 | 
				
			||||||
    if (backupAlbumInfo == null) {
 | 
					    if (backupAlbumInfo == null) {
 | 
				
			||||||
 | 
					      _clearErrorNotifications();
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await PhotoManager.setIgnorePermissionCheck(true);
 | 
					    await PhotoManager.setIgnorePermissionCheck(true);
 | 
				
			||||||
 | 
					    _errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (_canceledBySystem) {
 | 
					    if (_canceledBySystem) {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final List<AssetEntity> toUpload =
 | 
					    List<AssetEntity> toUpload =
 | 
				
			||||||
        await backupService.getAssetsToBackup(backupAlbumInfo);
 | 
					        await backupService.buildUploadCandidates(backupAlbumInfo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      toUpload = await backupService.removeAlreadyUploadedAssets(toUpload);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      _showErrorNotification(
 | 
				
			||||||
 | 
					        title: "backup_background_service_error_title".tr(),
 | 
				
			||||||
 | 
					        content: "backup_background_service_connection_failed_message".tr(),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (_canceledBySystem) {
 | 
					    if (_canceledBySystem) {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (toUpload.isEmpty) {
 | 
					    if (toUpload.isEmpty) {
 | 
				
			||||||
 | 
					      _clearErrorNotifications();
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -343,10 +385,16 @@ class BackgroundService {
 | 
				
			|||||||
      _onBackupError,
 | 
					      _onBackupError,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    if (ok) {
 | 
					    if (ok) {
 | 
				
			||||||
 | 
					      _clearErrorNotifications();
 | 
				
			||||||
      await box.put(
 | 
					      await box.put(
 | 
				
			||||||
        backupInfoKey,
 | 
					        backupInfoKey,
 | 
				
			||||||
        backupAlbumInfo,
 | 
					        backupAlbumInfo,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      _showErrorNotification(
 | 
				
			||||||
 | 
					        title: "backup_background_service_error_title".tr(),
 | 
				
			||||||
 | 
					        content: "backup_background_service_backup_failed_message".tr(),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return ok;
 | 
					    return ok;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -358,20 +406,48 @@ class BackgroundService {
 | 
				
			|||||||
  void _onProgress(int sent, int total) {}
 | 
					  void _onProgress(int sent, int total) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _onBackupError(ErrorUploadAsset errorAssetInfo) {
 | 
					  void _onBackupError(ErrorUploadAsset errorAssetInfo) {
 | 
				
			||||||
    showErrorNotification(
 | 
					    _showErrorNotification(
 | 
				
			||||||
      "backup_background_service_upload_failure_notification"
 | 
					      title: "Upload failed",
 | 
				
			||||||
 | 
					      content: "backup_background_service_upload_failure_notification"
 | 
				
			||||||
          .tr(args: [errorAssetInfo.fileName]),
 | 
					          .tr(args: [errorAssetInfo.fileName]),
 | 
				
			||||||
      errorAssetInfo.errorMessage,
 | 
					      individualTag: errorAssetInfo.id,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
 | 
					  void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
 | 
				
			||||||
    updateNotification(
 | 
					    _updateNotification(
 | 
				
			||||||
      title: "backup_background_service_in_progress_notification".tr(),
 | 
					      title: "backup_background_service_in_progress_notification".tr(),
 | 
				
			||||||
      content: "backup_background_service_current_upload_notification"
 | 
					      content: "backup_background_service_current_upload_notification"
 | 
				
			||||||
          .tr(args: [currentUploadAsset.fileName]),
 | 
					          .tr(args: [currentUploadAsset.fileName]),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _isErrorGracePeriodExceeded() {
 | 
				
			||||||
 | 
					    final int value = AppSettingsService()
 | 
				
			||||||
 | 
					        .getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod);
 | 
				
			||||||
 | 
					    if (value == 0) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } else if (value == 5) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final DateTime? failedSince =
 | 
				
			||||||
 | 
					        Hive.box(backgroundBackupInfoBox).get(backupFailedSince);
 | 
				
			||||||
 | 
					    if (failedSince == null) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final Duration duration = DateTime.now().difference(failedSince);
 | 
				
			||||||
 | 
					    if (value == 1) {
 | 
				
			||||||
 | 
					      return duration > const Duration(minutes: 30);
 | 
				
			||||||
 | 
					    } else if (value == 2) {
 | 
				
			||||||
 | 
					      return duration > const Duration(hours: 2);
 | 
				
			||||||
 | 
					    } else if (value == 3) {
 | 
				
			||||||
 | 
					      return duration > const Duration(hours: 8);
 | 
				
			||||||
 | 
					    } else if (value == 4) {
 | 
				
			||||||
 | 
					      return duration > const Duration(hours: 24);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    assert(false, "Invalid value");
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// entry point called by Kotlin/Java code; needs to be a top-level function
 | 
					/// entry point called by Kotlin/Java code; needs to be a top-level function
 | 
				
			||||||
 | 
				
			|||||||
@ -41,21 +41,8 @@ class BackupService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Returns all assets to backup from the backup info taking into account the
 | 
					  /// Returns all assets newer than the last successful backup per album
 | 
				
			||||||
  /// time of the last successfull backup per album
 | 
					  Future<List<AssetEntity>> buildUploadCandidates(
 | 
				
			||||||
  Future<List<AssetEntity>> getAssetsToBackup(
 | 
					 | 
				
			||||||
    HiveBackupAlbums backupAlbumInfo,
 | 
					 | 
				
			||||||
  ) async {
 | 
					 | 
				
			||||||
    final List<AssetEntity> candidates =
 | 
					 | 
				
			||||||
        await _buildUploadCandidates(backupAlbumInfo);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final List<AssetEntity> toUpload = candidates.isEmpty
 | 
					 | 
				
			||||||
        ? []
 | 
					 | 
				
			||||||
        : await _removeAlreadyUploadedAssets(candidates);
 | 
					 | 
				
			||||||
    return toUpload;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<List<AssetEntity>> _buildUploadCandidates(
 | 
					 | 
				
			||||||
    HiveBackupAlbums backupAlbums,
 | 
					    HiveBackupAlbums backupAlbums,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    final filter = FilterOptionGroup(
 | 
					    final filter = FilterOptionGroup(
 | 
				
			||||||
@ -147,7 +134,8 @@ class BackupService {
 | 
				
			|||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<List<AssetEntity>> _removeAlreadyUploadedAssets(
 | 
					  /// Returns a new list of assets not yet uploaded
 | 
				
			||||||
 | 
					  Future<List<AssetEntity>> removeAlreadyUploadedAssets(
 | 
				
			||||||
    List<AssetEntity> candidates,
 | 
					    List<AssetEntity> candidates,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
 | 
					    final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,8 @@ enum AppSettingsEnum<T> {
 | 
				
			|||||||
  threeStageLoading<bool>("threeStageLoading", false),
 | 
					  threeStageLoading<bool>("threeStageLoading", false),
 | 
				
			||||||
  themeMode<String>("themeMode", "system"), // "light","dark","system"
 | 
					  themeMode<String>("themeMode", "system"), // "light","dark","system"
 | 
				
			||||||
  tilesPerRow<int>("tilesPerRow", 4),
 | 
					  tilesPerRow<int>("tilesPerRow", 4),
 | 
				
			||||||
 | 
					  uploadErrorNotificationGracePeriod<int>(
 | 
				
			||||||
 | 
					      "uploadErrorNotificationGracePeriod", 2),
 | 
				
			||||||
  storageIndicator<bool>("storageIndicator", true);
 | 
					  storageIndicator<bool>("storageIndicator", true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const AppSettingsEnum(this.hiveKey, this.defaultValue);
 | 
					  const AppSettingsEnum(this.hiveKey, this.defaultValue);
 | 
				
			||||||
 | 
				
			|||||||
@ -56,6 +56,7 @@ class TilesPerRow extends HookConsumerWidget {
 | 
				
			|||||||
          max: 6,
 | 
					          max: 6,
 | 
				
			||||||
          divisions: 4,
 | 
					          divisions: 4,
 | 
				
			||||||
          label: "${itemsValue.value.toInt()}",
 | 
					          label: "${itemsValue.value.toInt()}",
 | 
				
			||||||
 | 
					          activeColor: Theme.of(context).primaryColor,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					import 'package:easy_localization/easy_localization.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/settings/providers/app_settings.provider.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationSetting extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  const NotificationSetting({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final appSettingService = ref.watch(appSettingsServiceProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final sliderValue = useState(0.0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(
 | 
				
			||||||
 | 
					      () {
 | 
				
			||||||
 | 
					        sliderValue.value = appSettingService
 | 
				
			||||||
 | 
					            .getSetting<int>(AppSettingsEnum.uploadErrorNotificationGracePeriod)
 | 
				
			||||||
 | 
					            .toDouble();
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final String formattedValue = _formatSliderValue(sliderValue.value);
 | 
				
			||||||
 | 
					    return ExpansionTile(
 | 
				
			||||||
 | 
					      textColor: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					      title: const Text(
 | 
				
			||||||
 | 
					        'setting_notifications_title',
 | 
				
			||||||
 | 
					        style: TextStyle(
 | 
				
			||||||
 | 
					          fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ).tr(),
 | 
				
			||||||
 | 
					      subtitle: const Text(
 | 
				
			||||||
 | 
					        'setting_notifications_subtitle',
 | 
				
			||||||
 | 
					        style: TextStyle(
 | 
				
			||||||
 | 
					          fontSize: 13,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ).tr(),
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        ListTile(
 | 
				
			||||||
 | 
					          isThreeLine: false,
 | 
				
			||||||
 | 
					          dense: true,
 | 
				
			||||||
 | 
					          title: const Text(
 | 
				
			||||||
 | 
					            'setting_notifications_notify_failures_grace_period',
 | 
				
			||||||
 | 
					            style: TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					          ).tr(args: [formattedValue]),
 | 
				
			||||||
 | 
					          subtitle: Slider(
 | 
				
			||||||
 | 
					            value: sliderValue.value,
 | 
				
			||||||
 | 
					            onChanged: (double v) => sliderValue.value = v,
 | 
				
			||||||
 | 
					            onChangeEnd: (double v) => appSettingService.setSetting(
 | 
				
			||||||
 | 
					                AppSettingsEnum.uploadErrorNotificationGracePeriod, v.toInt()),
 | 
				
			||||||
 | 
					            max: 5.0,
 | 
				
			||||||
 | 
					            divisions: 5,
 | 
				
			||||||
 | 
					            label: formattedValue,
 | 
				
			||||||
 | 
					            activeColor: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					String _formatSliderValue(double v) {
 | 
				
			||||||
 | 
					  if (v == 0.0) {
 | 
				
			||||||
 | 
					    return 'setting_notifications_notify_immediately'.tr();
 | 
				
			||||||
 | 
					  } else if (v == 1.0) {
 | 
				
			||||||
 | 
					    return 'setting_notifications_notify_minutes'.tr(args: const ['30']);
 | 
				
			||||||
 | 
					  } else if (v == 2.0) {
 | 
				
			||||||
 | 
					    return 'setting_notifications_notify_hours'.tr(args: const ['2']);
 | 
				
			||||||
 | 
					  } else if (v == 3.0) {
 | 
				
			||||||
 | 
					    return 'setting_notifications_notify_hours'.tr(args: const ['8']);
 | 
				
			||||||
 | 
					  } else if (v == 4.0) {
 | 
				
			||||||
 | 
					    return 'setting_notifications_notify_hours'.tr(args: const ['24']);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return 'setting_notifications_notify_never'.tr();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
 | 
					import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
 | 
					import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
 | 
					import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SettingsPage extends HookConsumerWidget {
 | 
					class SettingsPage extends HookConsumerWidget {
 | 
				
			||||||
@ -37,7 +38,8 @@ class SettingsPage extends HookConsumerWidget {
 | 
				
			|||||||
            tiles: [
 | 
					            tiles: [
 | 
				
			||||||
              const ImageViewerQualitySetting(),
 | 
					              const ImageViewerQualitySetting(),
 | 
				
			||||||
              const ThemeSetting(),
 | 
					              const ThemeSetting(),
 | 
				
			||||||
              const AssetListSettings()
 | 
					              const AssetListSettings(),
 | 
				
			||||||
 | 
					              const NotificationSetting(),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ).toList(),
 | 
					          ).toList(),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user