From a30ebab70ca48a97bcdeba3734ce86221eaa9337 Mon Sep 17 00:00:00 2001
From: Peter Ombodi
Date: Tue, 21 Jul 2026 23:18:47 +0300
Subject: [PATCH] fix(mobile): refresh asset stack after deleting viewer item
(#28164)
* fix(mobile): refresh asset stack after deleting viewer item
* fix(mobile): clamp stack index,
filter visible stack assets
* refactor(mobile): simplify stack asset refresh handling
* fix(mobile): revert changes in DeleteLocalActionButton
* style(mobile): reformat code
* fix(mobile): remove redundant setStack
---------
Co-authored-by: Peter Ombodi
---
mobile/lib/domain/models/events.model.dart | 6 ++++
.../repositories/remote_asset.repository.dart | 8 ++++-
.../delete_action_button.widget.dart | 14 ++++++--
.../asset_viewer/asset_page.widget.dart | 3 +-
.../asset_viewer/asset_stack.provider.dart | 4 +++
.../asset_viewer/asset_viewer.page.dart | 35 +++++++++++++++++++
6 files changed, 65 insertions(+), 5 deletions(-)
diff --git a/mobile/lib/domain/models/events.model.dart b/mobile/lib/domain/models/events.model.dart
index 9bbe00852e..ef034a0d03 100644
--- a/mobile/lib/domain/models/events.model.dart
+++ b/mobile/lib/domain/models/events.model.dart
@@ -24,6 +24,12 @@ class ViewerReloadAssetEvent extends Event {
const ViewerReloadAssetEvent();
}
+class ViewerStackAssetDeletedEvent extends Event {
+ final int stackIndex;
+
+ const ViewerStackAssetDeletedEvent({required this.stackIndex});
+}
+
// Multi-Select Events
class MultiSelectToggleEvent extends Event {
final bool isEnabled;
diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart
index f48d88d764..b2cecaca35 100644
--- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart
+++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart
@@ -71,7 +71,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
}
final query = _db.remoteAssetEntity.select()
- ..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not())
+ ..where(
+ (row) =>
+ row.stackId.equals(stackId) &
+ row.id.equals(asset.id).not() &
+ row.deletedAt.isNull() &
+ row.visibility.equalsValue(AssetVisibility.timeline),
+ )
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
return query.map((row) => row.toDto()).get();
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 92747c6d44..2a7c4ba9d7 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
@@ -2,11 +2,13 @@ import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
+import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
+import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -59,13 +61,19 @@ class DeleteActionButton extends ConsumerWidget {
}
}
- if (source == ActionSource.viewer) {
- EventStream.shared.emit(const ViewerReloadAssetEvent());
- }
+ final currentAsset = ref.read(assetViewerProvider).currentAsset;
+ final stackIndex = ref.read(assetViewerProvider).stackIndex;
final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source);
ref.read(multiSelectProvider.notifier).reset();
+ if (source == ActionSource.viewer && result.success) {
+ final shouldRefreshStack = currentAsset is RemoteAsset && currentAsset.stackId != null;
+ EventStream.shared.emit(
+ shouldRefreshStack ? ViewerStackAssetDeletedEvent(stackIndex: stackIndex) : const ViewerReloadAssetEvent(),
+ );
+ }
+
final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart
index 65b9b3c686..1ef22a891f 100644
--- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart
+++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart
@@ -413,7 +413,8 @@ class _AssetPageState extends ConsumerState {
final showAssetStack = ref.watch(timelineServiceProvider.select((s) => s.origin != TimelineOrigin.trash));
final stackChildren = showAssetStack ? ref.watch(stackChildrenNotifier(asset)).valueOrNull : null;
if (stackChildren != null && stackChildren.isNotEmpty) {
- displayAsset = stackChildren.elementAt(stackIndex);
+ final safeStackIndex = stackIndex.clamp(0, stackChildren.length - 1);
+ displayAsset = stackChildren.elementAt(safeStackIndex);
}
final isCurrent = currentAsset != null && currentAsset.refersToSameAsset(displayAsset);
diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart
index dae6db568c..c4e06e5ad9 100644
--- a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart
+++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart
@@ -11,6 +11,10 @@ class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier stack) {
+ state = AsyncData(stack);
+ }
}
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
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 25f99e2361..1065ebe25e 100644
--- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart
+++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -221,6 +222,8 @@ class _AssetViewerState extends ConsumerState {
_onTimelineReloadEvent();
case ViewerReloadAssetEvent():
_onViewerReloadEvent();
+ case ViewerStackAssetDeletedEvent event:
+ _onViewerStackAssetDeletedEvent(event);
default:
}
}
@@ -236,6 +239,33 @@ class _AssetViewerState extends ConsumerState {
_onAssetChanged(target);
}
+ Future _onViewerStackAssetDeletedEvent(ViewerStackAssetDeletedEvent event) async {
+ final timelineAsset = ref.read(timelineServiceProvider).getAssetSafe(_currentPage);
+ if (timelineAsset == null) {
+ _onViewerReloadEvent();
+ return;
+ }
+
+ final stackProvider = stackChildrenNotifier(timelineAsset);
+
+ ref.invalidate(stackProvider);
+ final stack = await ref.read(stackProvider.future);
+
+ if (!mounted) {
+ return;
+ }
+
+ if (stack.isEmpty) {
+ _onViewerReloadEvent();
+ return;
+ }
+
+ final targetIndex = math.min(event.stackIndex, stack.length - 1);
+ ref.read(assetViewerProvider.notifier)
+ ..setAsset(stack[targetIndex])
+ ..setStackIndex(targetIndex);
+ }
+
void _onTimelineReloadEvent() {
final timelineService = ref.read(timelineServiceProvider);
final totalAssets = timelineService.totalAssets;
@@ -252,6 +282,11 @@ class _AssetViewerState extends ConsumerState {
if (index != _currentPage) {
_pageController.jumpToPage(index);
_onAssetChanged(index);
+ } else if (currentAsset is RemoteAsset && currentAsset.stackId != null && assetIndex == null) {
+ final timelineAsset = timelineService.getAssetSafe(index);
+ if (timelineAsset is! RemoteAsset || currentAsset.stackId != timelineAsset.stackId) {
+ _onAssetChanged(index);
+ }
} else if (currentAsset != null && assetIndex == null) {
_onAssetChanged(index);
}