mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix: improve sync backup error indicator (#22527)
* fix: improve sync indicator error * prefer backup disabled icon before error --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
3c5a125762
commit
212649edf9
8 changed files with 81 additions and 58 deletions
|
|
@ -6,11 +6,12 @@ import 'package:immich_mobile/utils/isolate.dart';
|
||||||
import 'package:worker_manager/worker_manager.dart';
|
import 'package:worker_manager/worker_manager.dart';
|
||||||
|
|
||||||
typedef SyncCallback = void Function();
|
typedef SyncCallback = void Function();
|
||||||
|
typedef SyncCallbackWithResult<T> = void Function(T result);
|
||||||
typedef SyncErrorCallback = void Function(String error);
|
typedef SyncErrorCallback = void Function(String error);
|
||||||
|
|
||||||
class BackgroundSyncManager {
|
class BackgroundSyncManager {
|
||||||
final SyncCallback? onRemoteSyncStart;
|
final SyncCallback? onRemoteSyncStart;
|
||||||
final SyncCallback? onRemoteSyncComplete;
|
final SyncCallbackWithResult<bool?>? onRemoteSyncComplete;
|
||||||
final SyncErrorCallback? onRemoteSyncError;
|
final SyncErrorCallback? onRemoteSyncError;
|
||||||
|
|
||||||
final SyncCallback? onLocalSyncStart;
|
final SyncCallback? onLocalSyncStart;
|
||||||
|
|
@ -156,15 +157,18 @@ class BackgroundSyncManager {
|
||||||
debugLabel: 'remote-sync',
|
debugLabel: 'remote-sync',
|
||||||
);
|
);
|
||||||
return _syncTask!
|
return _syncTask!
|
||||||
.then((result) => result ?? false)
|
.then((result) {
|
||||||
.whenComplete(() {
|
final success = result ?? false;
|
||||||
onRemoteSyncComplete?.call();
|
onRemoteSyncComplete?.call(success);
|
||||||
_syncTask = null;
|
return success;
|
||||||
})
|
})
|
||||||
.catchError((error) {
|
.catchError((error) {
|
||||||
onRemoteSyncError?.call(error.toString());
|
onRemoteSyncError?.call(error.toString());
|
||||||
_syncTask = null;
|
_syncTask = null;
|
||||||
return false;
|
return false;
|
||||||
|
})
|
||||||
|
.whenComplete(() {
|
||||||
|
_syncTask = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||||
|
|
||||||
ref.read(driftBackupProvider.notifier).updateSyncing(true);
|
ref.read(driftBackupProvider.notifier).updateSyncing(true);
|
||||||
syncSuccess = await ref.read(backgroundSyncProvider).syncRemote();
|
syncSuccess = await ref.read(backgroundSyncProvider).syncRemote();
|
||||||
ref
|
|
||||||
.read(driftBackupProvider.notifier)
|
|
||||||
.updateError(syncSuccess == true ? BackupError.none : BackupError.syncFailed);
|
|
||||||
ref.read(driftBackupProvider.notifier).updateSyncing(false);
|
ref.read(driftBackupProvider.notifier).updateSyncing(false);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -94,7 +91,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||||
|
|
||||||
if (syncSuccess == false) {
|
if (syncSuccess == false) {
|
||||||
Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup");
|
Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup");
|
||||||
backupNotifier.updateError(BackupError.syncFailed);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await backupNotifier.startBackup(currentUser.id);
|
await backupNotifier.startBackup(currentUser.id);
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
return backupNotifier.startBackup(user.id);
|
return backupNotifier.startBackup(user.id);
|
||||||
} else {
|
} else {
|
||||||
Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup');
|
Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup');
|
||||||
backupNotifier.updateError(BackupError.syncFailed);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
||||||
return backupNotifier.startBackup(currentUser.id);
|
return backupNotifier.startBackup(currentUser.id);
|
||||||
} else {
|
} else {
|
||||||
Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup');
|
Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup');
|
||||||
backupNotifier.updateError(BackupError.syncFailed);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||||
_resumeBackup(backupProvider),
|
_resumeBackup(backupProvider),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
backupProvider.updateError(BackupError.syncFailed);
|
|
||||||
await backgroundManager.hashAssets();
|
await backgroundManager.hashAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
_resumeBackup(),
|
_resumeBackup(),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
_ref.read(driftBackupProvider.notifier).updateError(BackupError.syncFailed);
|
|
||||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,21 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
|
|
||||||
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||||
final syncStatusNotifier = ref.read(syncStatusProvider.notifier);
|
final syncStatusNotifier = ref.read(syncStatusProvider.notifier);
|
||||||
|
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||||
|
|
||||||
final manager = BackgroundSyncManager(
|
final manager = BackgroundSyncManager(
|
||||||
onRemoteSyncStart: syncStatusNotifier.startRemoteSync,
|
onRemoteSyncStart: () {
|
||||||
onRemoteSyncComplete: syncStatusNotifier.completeRemoteSync,
|
syncStatusNotifier.startRemoteSync();
|
||||||
|
backupProvider.updateError(BackupError.none);
|
||||||
|
},
|
||||||
|
onRemoteSyncComplete: (isSuccess) {
|
||||||
|
syncStatusNotifier.completeRemoteSync();
|
||||||
|
backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed);
|
||||||
|
},
|
||||||
onRemoteSyncError: syncStatusNotifier.errorRemoteSync,
|
onRemoteSyncError: syncStatusNotifier.errorRemoteSync,
|
||||||
onLocalSyncStart: syncStatusNotifier.startLocalSync,
|
onLocalSyncStart: syncStatusNotifier.startLocalSync,
|
||||||
onLocalSyncComplete: syncStatusNotifier.completeLocalSync,
|
onLocalSyncComplete: syncStatusNotifier.completeLocalSync,
|
||||||
|
|
|
||||||
|
|
@ -162,48 +162,32 @@ class _ProfileIndicator extends ConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const double _kBadgeWidgetSize = 30.0;
|
||||||
|
|
||||||
class _BackupIndicator extends ConsumerWidget {
|
class _BackupIndicator extends ConsumerWidget {
|
||||||
const _BackupIndicator();
|
const _BackupIndicator();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
const widgetSize = 30.0;
|
final indicatorIcon = _getBackupBadgeIcon(context, ref);
|
||||||
final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
|
|
||||||
final indicatorIcon = hasError
|
|
||||||
? Icon(
|
|
||||||
Icons.warning_rounded,
|
|
||||||
size: 12,
|
|
||||||
color: context.colorScheme.error,
|
|
||||||
semanticLabel: 'backup_controller_page_backup'.tr(),
|
|
||||||
)
|
|
||||||
: _getBackupBadgeIcon(context, ref);
|
|
||||||
final badgeBackground = hasError ? context.colorScheme.errorContainer : context.colorScheme.surfaceContainer;
|
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => context.pushRoute(const DriftBackupRoute()),
|
onTap: () => context.pushRoute(const DriftBackupRoute()),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
child: Badge(
|
child: Badge(
|
||||||
label: Container(
|
label: indicatorIcon,
|
||||||
width: widgetSize / 2,
|
|
||||||
height: widgetSize / 2,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: badgeBackground,
|
|
||||||
border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)),
|
|
||||||
borderRadius: BorderRadius.circular(widgetSize / 2),
|
|
||||||
),
|
|
||||||
child: indicatorIcon,
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
isLabelVisible: indicatorIcon != null,
|
isLabelVisible: indicatorIcon != null,
|
||||||
offset: const Offset(-2, -12),
|
offset: const Offset(-2, -12),
|
||||||
child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor),
|
child: Icon(Icons.backup_rounded, size: _kBadgeWidgetSize, color: context.primaryColor),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) {
|
Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) {
|
||||||
final backupStateStream = ref.watch(settingsProvider).watch(Setting.enableBackup);
|
final backupStateStream = ref.watch(settingsProvider).watch(Setting.enableBackup);
|
||||||
|
final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
|
||||||
final isDarkTheme = context.isDarkTheme;
|
final isDarkTheme = context.isDarkTheme;
|
||||||
final iconColor = isDarkTheme ? Colors.white : Colors.black;
|
final iconColor = isDarkTheme ? Colors.white : Colors.black;
|
||||||
final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty));
|
final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty));
|
||||||
|
|
@ -215,42 +199,76 @@ class _BackupIndicator extends ConsumerWidget {
|
||||||
final backupEnabled = snapshot.data ?? false;
|
final backupEnabled = snapshot.data ?? false;
|
||||||
|
|
||||||
if (!backupEnabled) {
|
if (!backupEnabled) {
|
||||||
return Icon(
|
return _BadgeLabel(
|
||||||
Icons.cloud_off_rounded,
|
Icon(
|
||||||
size: 9,
|
Icons.cloud_off_rounded,
|
||||||
color: iconColor,
|
size: 9,
|
||||||
semanticLabel: 'backup_controller_page_backup'.tr(),
|
color: iconColor,
|
||||||
|
semanticLabel: 'backup_controller_page_backup'.tr(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return _BadgeLabel(
|
||||||
|
Icon(
|
||||||
|
Icons.warning_rounded,
|
||||||
|
size: 12,
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
semanticLabel: 'backup_controller_page_backup'.tr(),
|
||||||
|
),
|
||||||
|
backgroundColor: context.colorScheme.errorContainer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUploading) {
|
if (isUploading) {
|
||||||
return Container(
|
return _BadgeLabel(
|
||||||
padding: const EdgeInsets.all(3.5),
|
Container(
|
||||||
child: Theme(
|
padding: const EdgeInsets.all(3.5),
|
||||||
data: context.themeData.copyWith(
|
child: Theme(
|
||||||
progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true),
|
data: context.themeData.copyWith(
|
||||||
),
|
progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true),
|
||||||
child: CircularProgressIndicator(
|
),
|
||||||
strokeWidth: 2,
|
child: CircularProgressIndicator(
|
||||||
strokeCap: StrokeCap.round,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(iconColor),
|
strokeCap: StrokeCap.round,
|
||||||
semanticsLabel: 'backup_controller_page_backup'.tr(),
|
valueColor: AlwaysStoppedAnimation<Color>(iconColor),
|
||||||
|
semanticsLabel: 'backup_controller_page_backup'.tr(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Icon(
|
return _BadgeLabel(
|
||||||
Icons.check_outlined,
|
Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
|
||||||
size: 9,
|
|
||||||
color: iconColor,
|
|
||||||
semanticLabel: 'backup_controller_page_backup'.tr(),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _BadgeLabel extends StatelessWidget {
|
||||||
|
final Widget indicator;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
const _BadgeLabel(this.indicator, {this.backgroundColor});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: _kBadgeWidgetSize / 2,
|
||||||
|
height: _kBadgeWidgetSize / 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor ?? context.colorScheme.surfaceContainer,
|
||||||
|
border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)),
|
||||||
|
borderRadius: BorderRadius.circular(_kBadgeWidgetSize / 2),
|
||||||
|
),
|
||||||
|
child: indicator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SyncStatusIndicator extends ConsumerStatefulWidget {
|
class _SyncStatusIndicator extends ConsumerStatefulWidget {
|
||||||
const _SyncStatusIndicator();
|
const _SyncStatusIndicator();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue