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); }