From 677cb660f55fe7d81888c1a794993f8c6c21e402 Mon Sep 17 00:00:00 2001 From: Thomas <9749173+uhthomas@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:43:14 +0000 Subject: [PATCH] fix(mobile): reflect asset deletions instantly (#26835) Sometimes the current asset won't update when deleted, or it won't refresh until an event (like showing details) happens. --- .../archive_action_button.widget.dart | 6 +- .../delete_action_button.widget.dart | 6 +- ...delete_permanent_action_button.widget.dart | 6 +- ...e_to_lock_folder_action_button.widget.dart | 6 +- ...emove_from_album_action_button.widget.dart | 6 +- .../trash_action_button.widget.dart | 6 +- .../unarchive_action_button.widget.dart | 6 +- .../asset_viewer/asset_viewer.page.dart | 64 ++++++++----------- 8 files changed, 47 insertions(+), 59 deletions(-) diff --git a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart index 7c7e96c1c5..a673dff1d7 100644 --- a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart @@ -14,13 +14,13 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; Future performArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { if (!context.mounted) return; - final result = await ref.read(actionProvider.notifier).archive(source); - ref.read(multiSelectProvider.notifier).reset(); - if (source == ActionSource.viewer) { EventStream.shared.emit(const ViewerReloadAssetEvent()); } + final result = await ref.read(actionProvider.notifier).archive(source); + ref.read(multiSelectProvider.notifier).reset(); + final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); if (context.mounted) { diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart index 94ee1b2343..2121ef3159 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart @@ -57,13 +57,13 @@ class DeleteActionButton extends ConsumerWidget { if (confirm != true) return; } - final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source); - ref.read(multiSelectProvider.notifier).reset(); - if (source == ActionSource.viewer) { EventStream.shared.emit(const ViewerReloadAssetEvent()); } + final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source); + ref.read(multiSelectProvider.notifier).reset(); + final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()}); if (context.mounted) { diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart index 710ec506c2..27a1a4d8af 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart @@ -35,13 +35,13 @@ class DeletePermanentActionButton extends ConsumerWidget { false; if (!confirm) return; - final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); - ref.read(multiSelectProvider.notifier).reset(); - if (source == ActionSource.viewer) { EventStream.shared.emit(const ViewerReloadAssetEvent()); } + final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); + ref.read(multiSelectProvider.notifier).reset(); + final successMessage = 'delete_permanently_action_prompt'.t( context: context, args: {'count': result.count.toString()}, diff --git a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart index 341791c1af..2f7c3899eb 100644 --- a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart @@ -14,13 +14,13 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; Future performMoveToLockFolderAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { if (!context.mounted) return; - final result = await ref.read(actionProvider.notifier).moveToLockFolder(source); - ref.read(multiSelectProvider.notifier).reset(); - if (source == ActionSource.viewer) { EventStream.shared.emit(const ViewerReloadAssetEvent()); } + final result = await ref.read(actionProvider.notifier).moveToLockFolder(source); + ref.read(multiSelectProvider.notifier).reset(); + final successMessage = 'move_to_lock_folder_action_prompt'.t( context: context, args: {'count': result.count.toString()}, diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart index fd88e94cf7..97a36a56dc 100644 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart @@ -29,13 +29,13 @@ class RemoveFromAlbumActionButton extends ConsumerWidget { return; } - final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId); - ref.read(multiSelectProvider.notifier).reset(); - if (source == ActionSource.viewer) { EventStream.shared.emit(const ViewerReloadAssetEvent()); } + final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId); + ref.read(multiSelectProvider.notifier).reset(); + final successMessage = 'remove_from_album_action_prompt'.t( context: context, args: {'count': result.count.toString()}, diff --git a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart index 5f1e385769..e95569af45 100644 --- a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart @@ -25,13 +25,13 @@ class TrashActionButton extends ConsumerWidget { return; } - final result = await ref.read(actionProvider.notifier).trash(source); - ref.read(multiSelectProvider.notifier).reset(); - if (source == ActionSource.viewer) { EventStream.shared.emit(const ViewerReloadAssetEvent()); } + final result = await ref.read(actionProvider.notifier).trash(source); + ref.read(multiSelectProvider.notifier).reset(); + final successMessage = 'trash_action_prompt'.t(context: context, args: {'count': result.count.toString()}); if (context.mounted) { diff --git a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart index 8cf0bcba92..98e868d953 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart @@ -16,13 +16,13 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; Future performUnArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { if (!context.mounted) return; - final result = await ref.read(actionProvider.notifier).unArchive(source); - ref.read(multiSelectProvider.notifier).reset(); - if (source == ActionSource.viewer) { EventStream.shared.emit(const ViewerReloadAssetEvent()); } + final result = await ref.read(actionProvider.notifier).unArchive(source); + ref.read(multiSelectProvider.notifier).reset(); + final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); if (context.mounted) { diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 903105406c..4d8954d4ef 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -81,19 +81,17 @@ class _AssetViewerState extends ConsumerState { late final _preloader = AssetPreloader(timelineService: ref.read(timelineServiceProvider), mounted: () => mounted); late int _currentPage = widget.initialIndex; + late int _totalAssets = ref.read(timelineServiceProvider).totalAssets; StreamSubscription? _reloadSubscription; KeepAliveLink? _stackChildrenKeepAlive; - bool _assetReloadRequested = false; - void _onTapNavigate(int direction) { final page = _pageController.page?.toInt(); if (page == null) return; final target = page + direction; - final maxPage = ref.read(timelineServiceProvider).totalAssets - 1; + final maxPage = _totalAssets - 1; if (target >= 0 && target <= maxPage) { - _currentPage = target; _pageController.jumpToPage(target); _onAssetChanged(target); } @@ -141,7 +139,6 @@ class _AssetViewerState extends ConsumerState { final page = _pageController.page?.round(); if (page != null && page != _currentPage) { - _currentPage = page; _onAssetChanged(page); } return false; @@ -153,8 +150,9 @@ class _AssetViewerState extends ConsumerState { } void _onAssetChanged(int index) async { - final timelineService = ref.read(timelineServiceProvider); - final asset = await timelineService.getAssetAsync(index); + _currentPage = index; + + final asset = await ref.read(timelineServiceProvider).getAssetAsync(index); if (asset == null) return; AssetViewer._setAsset(ref, asset); @@ -193,11 +191,20 @@ class _AssetViewerState extends ConsumerState { case TimelineReloadEvent(): _onTimelineReloadEvent(); case ViewerReloadAssetEvent(): - _assetReloadRequested = true; + _onViewerReloadEvent(); default: } } + void _onViewerReloadEvent() { + if (_totalAssets <= 1) return; + + final index = _pageController.page?.round() ?? 0; + final target = index >= _totalAssets - 1 ? index - 1 : index + 1; + _pageController.animateToPage(target, duration: Durations.medium1, curve: Curves.easeInOut); + _onAssetChanged(target); + } + void _onTimelineReloadEvent() { final timelineService = ref.read(timelineServiceProvider); final totalAssets = timelineService.totalAssets; @@ -207,43 +214,24 @@ class _AssetViewerState extends ConsumerState { return; } - var index = _pageController.page?.round() ?? 0; final currentAsset = ref.read(assetViewerProvider).currentAsset; - if (currentAsset != null) { - final newIndex = timelineService.getIndex(currentAsset.heroTag); - if (newIndex != null && newIndex != index) { - index = newIndex; - _currentPage = index; - _pageController.jumpToPage(index); - } - } + final assetIndex = currentAsset != null ? timelineService.getIndex(currentAsset.heroTag) : null; + final index = (assetIndex ?? _currentPage).clamp(0, totalAssets - 1); - if (index >= totalAssets) { - index = totalAssets - 1; - _currentPage = index; + if (index != _currentPage) { _pageController.jumpToPage(index); + _onAssetChanged(index); + } else if (currentAsset != null && assetIndex == null) { + _onAssetChanged(index); } - if (_assetReloadRequested) { - _assetReloadRequested = false; - _onAssetReloadEvent(index); + if (_totalAssets != totalAssets) { + setState(() { + _totalAssets = totalAssets; + }); } } - void _onAssetReloadEvent(int index) async { - final timelineService = ref.read(timelineServiceProvider); - - final newAsset = await timelineService.getAssetAsync(index); - if (newAsset == null) return; - - final currentAsset = ref.read(assetViewerProvider).currentAsset; - - // Do not reload if the asset has not changed - if (newAsset.heroTag == currentAsset?.heroTag) return; - - _onAssetChanged(index); - } - void _setSystemUIMode(bool controls, bool details) { final mode = !controls || (CurrentPlatform.isIOS && details) ? SystemUiMode.immersiveSticky @@ -301,7 +289,7 @@ class _AssetViewerState extends ConsumerState { : CurrentPlatform.isIOS ? const FastScrollPhysics() : const FastClampingScrollPhysics(), - itemCount: ref.read(timelineServiceProvider).totalAssets, + itemCount: _totalAssets, itemBuilder: (context, index) => AssetPage(index: index, heroOffset: _heroOffset, onTapNavigate: _onTapNavigate), ),