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.
This commit is contained in:
Thomas
2026-03-17 11:43:14 +00:00
committed by GitHub
parent 9b0b2bfcf2
commit 677cb660f5
8 changed files with 47 additions and 59 deletions

View File

@@ -14,13 +14,13 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
Future<void> performArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { Future<void> performArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
if (!context.mounted) return; if (!context.mounted) return;
final result = await ref.read(actionProvider.notifier).archive(source);
ref.read(multiSelectProvider.notifier).reset();
if (source == ActionSource.viewer) { if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent()); 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()}); final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) { if (context.mounted) {

View File

@@ -57,13 +57,13 @@ class DeleteActionButton extends ConsumerWidget {
if (confirm != true) return; if (confirm != true) return;
} }
final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source);
ref.read(multiSelectProvider.notifier).reset();
if (source == ActionSource.viewer) { if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent()); 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()}); final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) { if (context.mounted) {

View File

@@ -35,13 +35,13 @@ class DeletePermanentActionButton extends ConsumerWidget {
false; false;
if (!confirm) return; if (!confirm) return;
final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source);
ref.read(multiSelectProvider.notifier).reset();
if (source == ActionSource.viewer) { if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent()); 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( final successMessage = 'delete_permanently_action_prompt'.t(
context: context, context: context,
args: {'count': result.count.toString()}, args: {'count': result.count.toString()},

View File

@@ -14,13 +14,13 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
Future<void> performMoveToLockFolderAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { Future<void> performMoveToLockFolderAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
if (!context.mounted) return; if (!context.mounted) return;
final result = await ref.read(actionProvider.notifier).moveToLockFolder(source);
ref.read(multiSelectProvider.notifier).reset();
if (source == ActionSource.viewer) { if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent()); 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( final successMessage = 'move_to_lock_folder_action_prompt'.t(
context: context, context: context,
args: {'count': result.count.toString()}, args: {'count': result.count.toString()},

View File

@@ -29,13 +29,13 @@ class RemoveFromAlbumActionButton extends ConsumerWidget {
return; return;
} }
final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId);
ref.read(multiSelectProvider.notifier).reset();
if (source == ActionSource.viewer) { if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent()); 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( final successMessage = 'remove_from_album_action_prompt'.t(
context: context, context: context,
args: {'count': result.count.toString()}, args: {'count': result.count.toString()},

View File

@@ -25,13 +25,13 @@ class TrashActionButton extends ConsumerWidget {
return; return;
} }
final result = await ref.read(actionProvider.notifier).trash(source);
ref.read(multiSelectProvider.notifier).reset();
if (source == ActionSource.viewer) { if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent()); 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()}); final successMessage = 'trash_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) { if (context.mounted) {

View File

@@ -16,13 +16,13 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
Future<void> performUnArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { Future<void> performUnArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
if (!context.mounted) return; if (!context.mounted) return;
final result = await ref.read(actionProvider.notifier).unArchive(source);
ref.read(multiSelectProvider.notifier).reset();
if (source == ActionSource.viewer) { if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent()); 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()}); final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) { if (context.mounted) {

View File

@@ -81,19 +81,17 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
late final _preloader = AssetPreloader(timelineService: ref.read(timelineServiceProvider), mounted: () => mounted); late final _preloader = AssetPreloader(timelineService: ref.read(timelineServiceProvider), mounted: () => mounted);
late int _currentPage = widget.initialIndex; late int _currentPage = widget.initialIndex;
late int _totalAssets = ref.read(timelineServiceProvider).totalAssets;
StreamSubscription? _reloadSubscription; StreamSubscription? _reloadSubscription;
KeepAliveLink? _stackChildrenKeepAlive; KeepAliveLink? _stackChildrenKeepAlive;
bool _assetReloadRequested = false;
void _onTapNavigate(int direction) { void _onTapNavigate(int direction) {
final page = _pageController.page?.toInt(); final page = _pageController.page?.toInt();
if (page == null) return; if (page == null) return;
final target = page + direction; final target = page + direction;
final maxPage = ref.read(timelineServiceProvider).totalAssets - 1; final maxPage = _totalAssets - 1;
if (target >= 0 && target <= maxPage) { if (target >= 0 && target <= maxPage) {
_currentPage = target;
_pageController.jumpToPage(target); _pageController.jumpToPage(target);
_onAssetChanged(target); _onAssetChanged(target);
} }
@@ -141,7 +139,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
final page = _pageController.page?.round(); final page = _pageController.page?.round();
if (page != null && page != _currentPage) { if (page != null && page != _currentPage) {
_currentPage = page;
_onAssetChanged(page); _onAssetChanged(page);
} }
return false; return false;
@@ -153,8 +150,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
} }
void _onAssetChanged(int index) async { void _onAssetChanged(int index) async {
final timelineService = ref.read(timelineServiceProvider); _currentPage = index;
final asset = await timelineService.getAssetAsync(index);
final asset = await ref.read(timelineServiceProvider).getAssetAsync(index);
if (asset == null) return; if (asset == null) return;
AssetViewer._setAsset(ref, asset); AssetViewer._setAsset(ref, asset);
@@ -193,11 +191,20 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
case TimelineReloadEvent(): case TimelineReloadEvent():
_onTimelineReloadEvent(); _onTimelineReloadEvent();
case ViewerReloadAssetEvent(): case ViewerReloadAssetEvent():
_assetReloadRequested = true; _onViewerReloadEvent();
default: 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() { void _onTimelineReloadEvent() {
final timelineService = ref.read(timelineServiceProvider); final timelineService = ref.read(timelineServiceProvider);
final totalAssets = timelineService.totalAssets; final totalAssets = timelineService.totalAssets;
@@ -207,41 +214,22 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
return; return;
} }
var index = _pageController.page?.round() ?? 0;
final currentAsset = ref.read(assetViewerProvider).currentAsset; final currentAsset = ref.read(assetViewerProvider).currentAsset;
if (currentAsset != null) { final assetIndex = currentAsset != null ? timelineService.getIndex(currentAsset.heroTag) : null;
final newIndex = timelineService.getIndex(currentAsset.heroTag); final index = (assetIndex ?? _currentPage).clamp(0, totalAssets - 1);
if (newIndex != null && newIndex != index) {
index = newIndex; if (index != _currentPage) {
_currentPage = index;
_pageController.jumpToPage(index); _pageController.jumpToPage(index);
}
}
if (index >= totalAssets) {
index = totalAssets - 1;
_currentPage = index;
_pageController.jumpToPage(index);
}
if (_assetReloadRequested) {
_assetReloadRequested = false;
_onAssetReloadEvent(index);
}
}
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); _onAssetChanged(index);
} else if (currentAsset != null && assetIndex == null) {
_onAssetChanged(index);
}
if (_totalAssets != totalAssets) {
setState(() {
_totalAssets = totalAssets;
});
}
} }
void _setSystemUIMode(bool controls, bool details) { void _setSystemUIMode(bool controls, bool details) {
@@ -301,7 +289,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
: CurrentPlatform.isIOS : CurrentPlatform.isIOS
? const FastScrollPhysics() ? const FastScrollPhysics()
: const FastClampingScrollPhysics(), : const FastClampingScrollPhysics(),
itemCount: ref.read(timelineServiceProvider).totalAssets, itemCount: _totalAssets,
itemBuilder: (context, index) => itemBuilder: (context, index) =>
AssetPage(index: index, heroOffset: _heroOffset, onTapNavigate: _onTapNavigate), AssetPage(index: index, heroOffset: _heroOffset, onTapNavigate: _onTapNavigate),
), ),