From b74cfd4424b2bc677038a0cd10400233dfb142d1 Mon Sep 17 00:00:00 2001
From: Peter Ombodi
Date: Wed, 29 Apr 2026 13:49:47 +0300
Subject: [PATCH] fix(mobile): suppress asset stack UI in trash timeline
(#26536)
* fix(mobile): suppress asset stack UI in trash timeline
* refactor(mobile): apply review suggestions
* fix(mobile): hide unstack action in the trash timeline
* fix(mobile): move stack indicator out of asset type icons
---------
Co-authored-by: Peter Ombodi
---
.../asset_viewer/asset_page.widget.dart | 4 +-
.../asset_viewer/asset_stack.widget.dart | 7 ++++
.../widgets/images/thumbnail_tile.widget.dart | 38 +++++++++++++++----
.../widgets/timeline/fixed/segment.model.dart | 2 +
mobile/lib/utils/action_button.utils.dart | 1 +
5 files changed, 43 insertions(+), 9 deletions(-)
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 0934536471..bfd9738dc7 100644
--- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart
+++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/scroll_extensions.dart';
@@ -363,7 +364,8 @@ class _AssetPageState extends ConsumerState {
}
BaseAsset displayAsset = asset;
- final stackChildren = ref.watch(stackChildrenNotifier(asset)).valueOrNull;
+ 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);
}
diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart
index 213dc92ef3..f5d75a6a86 100644
--- a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart
+++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
+import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
+import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
class AssetStackRow extends ConsumerWidget {
final List stack;
@@ -15,6 +17,11 @@ class AssetStackRow extends ConsumerWidget {
return const SizedBox.shrink();
}
+ final hideAssetStack = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
+ if (hideAssetStack) {
+ return const SizedBox.shrink();
+ }
+
final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0);
diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart
index 5746414361..406ca30820 100644
--- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart
+++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart
@@ -21,6 +21,7 @@ class ThumbnailTile extends ConsumerStatefulWidget {
this.showStorageIndicator = false,
this.lockSelection = false,
this.heroOffset,
+ this.showStackIndicator = false,
super.key,
});
@@ -30,6 +31,7 @@ class ThumbnailTile extends ConsumerStatefulWidget {
final bool showStorageIndicator;
final bool lockSelection;
final int? heroOffset;
+ final bool showStackIndicator;
@override
ConsumerState createState() => _ThumbnailTileState();
@@ -139,7 +141,14 @@ class _ThumbnailTileState extends ConsumerState {
duration: Durations.short4,
child: Align(
alignment: Alignment.topRight,
- child: _AssetTypeIcons(asset: asset),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ _AssetTypeIcons(asset: asset),
+ if (widget.showStackIndicator) _StackIndicator(asset: asset),
+ ],
+ ),
),
),
if (storageIndicator && asset != null)
@@ -286,8 +295,8 @@ class _AssetTypeIcons extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
- final isLivePhoto = asset is RemoteAsset && asset.livePhotoVideoId != null;
+ final remoteAsset = asset is RemoteAsset ? asset as RemoteAsset : null;
+ final isLivePhoto = remoteAsset?.livePhotoVideoId != null;
return Column(
mainAxisSize: MainAxisSize.min,
@@ -295,11 +304,6 @@ class _AssetTypeIcons extends StatelessWidget {
children: [
if (asset.isVideo)
Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)),
- if (hasStack)
- const Padding(
- padding: EdgeInsets.only(right: 10.0, top: 6.0),
- child: _TileOverlayIcon(Icons.burst_mode_rounded),
- ),
if (isLivePhoto)
const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0),
@@ -312,6 +316,24 @@ class _AssetTypeIcons extends StatelessWidget {
}
}
+class _StackIndicator extends StatelessWidget {
+ final BaseAsset asset;
+
+ const _StackIndicator({required this.asset});
+
+ @override
+ Widget build(BuildContext context) {
+ if (asset is! RemoteAsset || (asset as RemoteAsset).stackId == null) {
+ return const SizedBox.shrink();
+ }
+
+ return const Padding(
+ padding: EdgeInsets.only(right: 10.0, top: 6.0),
+ child: _TileOverlayIcon(Icons.burst_mode_rounded),
+ );
+ }
+}
+
class _UploadProgressOverlay extends StatelessWidget {
final double progress;
diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart
index aa2112b8dd..c62a4946c7 100644
--- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart
+++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart
@@ -244,6 +244,7 @@ class _AssetTileWidget extends ConsumerWidget {
final lockSelection = _getLockSelectionStatus(ref);
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
+ final showStackIndicator = ref.read(timelineServiceProvider).origin != TimelineOrigin.trash;
return RepaintBoundary(
child: GestureDetector(
@@ -253,6 +254,7 @@ class _AssetTileWidget extends ConsumerWidget {
asset,
lockSelection: lockSelection,
showStorageIndicator: showStorageIndicator,
+ showStackIndicator: showStackIndicator,
heroOffset: heroOffset,
),
),
diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart
index 3edc50c847..4f7ad83093 100644
--- a/mobile/lib/utils/action_button.utils.dart
+++ b/mobile/lib/utils/action_button.utils.dart
@@ -148,6 +148,7 @@ enum ActionButtonType {
context.selectedCount == 1,
ActionButtonType.unstack =>
context.isOwner && //
+ context.timelineOrigin != TimelineOrigin.trash &&
!context.isInLockedView && //
context.isStacked,
ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView,