mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							c436c57cc9
						
					
				
				
					commit
					3125d04f32
				
			@@ -138,6 +138,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
			
		||||
                immediate = true,
 | 
			
		||||
                requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
 | 
			
		||||
                requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
 | 
			
		||||
                initialDelayInMs = ONE_MINUTE,
 | 
			
		||||
                retries = inputData.getInt(DATA_KEY_RETRIES, 0) + 1)
 | 
			
		||||
        }
 | 
			
		||||
        engine?.destroy()
 | 
			
		||||
@@ -169,6 +170,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
			
		||||
                                    immediate = true,
 | 
			
		||||
                                    requireUnmeteredNetwork = inputData.getBoolean(DATA_KEY_UNMETERED, true),
 | 
			
		||||
                                    requireCharging = inputData.getBoolean(DATA_KEY_CHARGING, false),
 | 
			
		||||
                                    initialDelayInMs = ONE_MINUTE,
 | 
			
		||||
                                    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 title = args.get(0) 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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
           .setContentTitle(title)
 | 
			
		||||
           .setTicker(title)
 | 
			
		||||
           .setContentText(content)
 | 
			
		||||
           .setSmallIcon(R.mipmap.ic_launcher)
 | 
			
		||||
           .setAutoCancel(true)
 | 
			
		||||
           .setOnlyAlertOnce(true)
 | 
			
		||||
           .build()
 | 
			
		||||
        val notificationId = SystemClock.uptimeMillis() as Int
 | 
			
		||||
        notificationManager.notify(notificationId, notification)
 | 
			
		||||
        notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun clearErrorNotifications() {
 | 
			
		||||
        notificationManager.cancel(NOTIFICATION_ERROR_ID)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
           .setOngoing(true)
 | 
			
		||||
           .build()
 | 
			
		||||
       return ForegroundInfo(1, notification)
 | 
			
		||||
       return ForegroundInfo(NOTIFICATION_ID, notification)
 | 
			
		||||
   }
 | 
			
		||||
 | 
			
		||||
    @RequiresApi(Build.VERSION_CODES.O)
 | 
			
		||||
    private fun createChannel() {
 | 
			
		||||
        val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -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_ERROR_ID = "immich/backgroundServiceError"
 | 
			
		||||
        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.
 | 
			
		||||
@@ -262,6 +272,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
			
		||||
                                    keepExisting: Boolean = false,
 | 
			
		||||
                                    requireUnmeteredNetwork: Boolean = false,
 | 
			
		||||
                                    requireCharging: Boolean = false,
 | 
			
		||||
                                    initialDelayInMs: Long = 0,
 | 
			
		||||
                                    retries: Int = 0) {
 | 
			
		||||
            if (!isEnabled(context)) {
 | 
			
		||||
                return
 | 
			
		||||
@@ -287,9 +298,10 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
 | 
			
		||||
            val photoCheck = OneTimeWorkRequest.Builder(BackupWorker::class.java)
 | 
			
		||||
                .setConstraints(constraints.build())
 | 
			
		||||
                .setInputData(inputData)
 | 
			
		||||
                .setInitialDelay(initialDelayInMs, TimeUnit.MILLISECONDS)
 | 
			
		||||
                .setBackoffCriteria(
 | 
			
		||||
                    BackoffPolicy.EXPONENTIAL,
 | 
			
		||||
                    OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
 | 
			
		||||
                    ONE_MINUTE,
 | 
			
		||||
                    TimeUnit.MILLISECONDS)
 | 
			
		||||
                .build()
 | 
			
		||||
            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_in_progress_notification": "Backing up your assets…",
 | 
			
		||||
  "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_backup": "Backup",
 | 
			
		||||
  "backup_controller_page_backup_selected": "Selected: ",
 | 
			
		||||
@@ -139,5 +142,12 @@
 | 
			
		||||
  "asset_list_settings_title": "Photo Grid",
 | 
			
		||||
  "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_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
 | 
			
		||||
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:ui' show IsolateNameServer, PluginUtilities;
 | 
			
		||||
import 'package:cancellation_token_http/http.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/services.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/services/backup.service.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:photo_manager/photo_manager.dart';
 | 
			
		||||
 | 
			
		||||
@@ -39,6 +41,7 @@ class BackgroundService {
 | 
			
		||||
  bool _hasLock = false;
 | 
			
		||||
  SendPort? _waitingIsolate;
 | 
			
		||||
  ReceivePort? _rp;
 | 
			
		||||
  bool _errorGracePeriodExceeded = true;
 | 
			
		||||
 | 
			
		||||
  bool get isForegroundInitialized {
 | 
			
		||||
    return _isForegroundInitialized;
 | 
			
		||||
@@ -140,8 +143,8 @@ class BackgroundService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Updates the notification shown by the background service
 | 
			
		||||
  Future<bool> updateNotification({
 | 
			
		||||
    String title = "Immich",
 | 
			
		||||
  Future<bool> _updateNotification({
 | 
			
		||||
    required String title,
 | 
			
		||||
    String? content,
 | 
			
		||||
  }) async {
 | 
			
		||||
    if (!Platform.isAndroid) {
 | 
			
		||||
@@ -153,28 +156,44 @@ class BackgroundService {
 | 
			
		||||
            .invokeMethod('updateNotification', [title, content]);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      debugPrint("[updateNotification] failed to communicate with plugin");
 | 
			
		||||
      debugPrint("[_updateNotification] failed to communicate with plugin");
 | 
			
		||||
    }
 | 
			
		||||
    return Future.value(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Shows a new priority notification
 | 
			
		||||
  Future<bool> showErrorNotification(
 | 
			
		||||
    String title,
 | 
			
		||||
    String content,
 | 
			
		||||
  ) async {
 | 
			
		||||
  Future<bool> _showErrorNotification({
 | 
			
		||||
    required String title,
 | 
			
		||||
    String? content,
 | 
			
		||||
    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) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      if (_isBackgroundInitialized) {
 | 
			
		||||
        return await _backgroundChannel
 | 
			
		||||
            .invokeMethod('showError', [title, content]);
 | 
			
		||||
        return await _backgroundChannel.invokeMethod('clearErrorNotifications');
 | 
			
		||||
      }
 | 
			
		||||
    } 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
 | 
			
		||||
@@ -278,7 +297,15 @@ class BackgroundService {
 | 
			
		||||
            return false;
 | 
			
		||||
          }
 | 
			
		||||
          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) {
 | 
			
		||||
          debugPrint(error.toString());
 | 
			
		||||
          return false;
 | 
			
		||||
@@ -303,6 +330,8 @@ class BackgroundService {
 | 
			
		||||
    Hive.registerAdapter(HiveBackupAlbumsAdapter());
 | 
			
		||||
    await Hive.openBox(userInfoBox);
 | 
			
		||||
    await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
 | 
			
		||||
    await Hive.openBox(userSettingInfoBox);
 | 
			
		||||
    await Hive.openBox(backgroundBackupInfoBox);
 | 
			
		||||
 | 
			
		||||
    ApiService apiService = ApiService();
 | 
			
		||||
    apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
 | 
			
		||||
@@ -313,23 +342,36 @@ class BackgroundService {
 | 
			
		||||
        await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
			
		||||
    final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
 | 
			
		||||
    if (backupAlbumInfo == null) {
 | 
			
		||||
      _clearErrorNotifications();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await PhotoManager.setIgnorePermissionCheck(true);
 | 
			
		||||
    _errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
 | 
			
		||||
 | 
			
		||||
    if (_canceledBySystem) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final List<AssetEntity> toUpload =
 | 
			
		||||
        await backupService.getAssetsToBackup(backupAlbumInfo);
 | 
			
		||||
    List<AssetEntity> toUpload =
 | 
			
		||||
        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) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (toUpload.isEmpty) {
 | 
			
		||||
      _clearErrorNotifications();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -343,10 +385,16 @@ class BackgroundService {
 | 
			
		||||
      _onBackupError,
 | 
			
		||||
    );
 | 
			
		||||
    if (ok) {
 | 
			
		||||
      _clearErrorNotifications();
 | 
			
		||||
      await box.put(
 | 
			
		||||
        backupInfoKey,
 | 
			
		||||
        backupAlbumInfo,
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      _showErrorNotification(
 | 
			
		||||
        title: "backup_background_service_error_title".tr(),
 | 
			
		||||
        content: "backup_background_service_backup_failed_message".tr(),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return ok;
 | 
			
		||||
  }
 | 
			
		||||
@@ -358,20 +406,48 @@ class BackgroundService {
 | 
			
		||||
  void _onProgress(int sent, int total) {}
 | 
			
		||||
 | 
			
		||||
  void _onBackupError(ErrorUploadAsset errorAssetInfo) {
 | 
			
		||||
    showErrorNotification(
 | 
			
		||||
      "backup_background_service_upload_failure_notification"
 | 
			
		||||
    _showErrorNotification(
 | 
			
		||||
      title: "Upload failed",
 | 
			
		||||
      content: "backup_background_service_upload_failure_notification"
 | 
			
		||||
          .tr(args: [errorAssetInfo.fileName]),
 | 
			
		||||
      errorAssetInfo.errorMessage,
 | 
			
		||||
      individualTag: errorAssetInfo.id,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
 | 
			
		||||
    updateNotification(
 | 
			
		||||
    _updateNotification(
 | 
			
		||||
      title: "backup_background_service_in_progress_notification".tr(),
 | 
			
		||||
      content: "backup_background_service_current_upload_notification"
 | 
			
		||||
          .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
 | 
			
		||||
 
 | 
			
		||||
@@ -41,21 +41,8 @@ class BackupService {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Returns all assets to backup from the backup info taking into account the
 | 
			
		||||
  /// time of the last successfull backup per album
 | 
			
		||||
  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(
 | 
			
		||||
  /// Returns all assets newer than the last successful backup per album
 | 
			
		||||
  Future<List<AssetEntity>> buildUploadCandidates(
 | 
			
		||||
    HiveBackupAlbums backupAlbums,
 | 
			
		||||
  ) async {
 | 
			
		||||
    final filter = FilterOptionGroup(
 | 
			
		||||
@@ -147,7 +134,8 @@ class BackupService {
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<AssetEntity>> _removeAlreadyUploadedAssets(
 | 
			
		||||
  /// Returns a new list of assets not yet uploaded
 | 
			
		||||
  Future<List<AssetEntity>> removeAlreadyUploadedAssets(
 | 
			
		||||
    List<AssetEntity> candidates,
 | 
			
		||||
  ) async {
 | 
			
		||||
    final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ enum AppSettingsEnum<T> {
 | 
			
		||||
  threeStageLoading<bool>("threeStageLoading", false),
 | 
			
		||||
  themeMode<String>("themeMode", "system"), // "light","dark","system"
 | 
			
		||||
  tilesPerRow<int>("tilesPerRow", 4),
 | 
			
		||||
  uploadErrorNotificationGracePeriod<int>(
 | 
			
		||||
      "uploadErrorNotificationGracePeriod", 2),
 | 
			
		||||
  storageIndicator<bool>("storageIndicator", true);
 | 
			
		||||
 | 
			
		||||
  const AppSettingsEnum(this.hiveKey, this.defaultValue);
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,7 @@ class TilesPerRow extends HookConsumerWidget {
 | 
			
		||||
          max: 6,
 | 
			
		||||
          divisions: 4,
 | 
			
		||||
          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: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/notification_setting/notification_setting.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
 | 
			
		||||
 | 
			
		||||
class SettingsPage extends HookConsumerWidget {
 | 
			
		||||
@@ -37,7 +38,8 @@ class SettingsPage extends HookConsumerWidget {
 | 
			
		||||
            tiles: [
 | 
			
		||||
              const ImageViewerQualitySetting(),
 | 
			
		||||
              const ThemeSetting(),
 | 
			
		||||
              const AssetListSettings()
 | 
			
		||||
              const AssetListSettings(),
 | 
			
		||||
              const NotificationSetting(),
 | 
			
		||||
            ],
 | 
			
		||||
          ).toList(),
 | 
			
		||||
        ],
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user