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 5e60d754fd..6c974376a1 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -96,6 +96,11 @@ class _AssetPageState extends ConsumerState { switch (event) { case ViewerShowDetailsEvent(): _showDetails(); + case TimelineReloadEvent(): + final asset = ref.read(timelineServiceProvider).getAssetSafe(widget.index); + if (asset != _asset) { + setState(() => _asset = asset); + } default: } } diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 95c665560e..ed62b9a0e8 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -353,6 +353,10 @@ class ActionNotifier extends Notifier { return null; } + if (source == ActionSource.viewer) { + ref.invalidate(assetExifProvider); + } + return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to edit date and time for assets', error, stack); diff --git a/mobile/test/providers/infrastructure/action_provider_test.dart b/mobile/test/providers/infrastructure/action_provider_test.dart new file mode 100644 index 0000000000..be13e58f73 --- /dev/null +++ b/mobile/test/providers/infrastructure/action_provider_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.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/user.model.dart'; +import 'package:immich_mobile/domain/services/asset.service.dart'; +import 'package:immich_mobile/domain/services/user.service.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/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/services/action.service.dart'; +import 'package:immich_mobile/services/download.service.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockActionService extends Mock implements ActionService {} + +class MockAssetService extends Mock implements AssetService {} + +class MockDownloadService extends Mock implements DownloadService {} + +class MockForegroundUploadService extends Mock implements ForegroundUploadService {} + +class MockUserService extends Mock implements UserService {} + +class FakeBuildContext extends Fake implements BuildContext {} + +final _user = UserDto(id: 'user-1', email: 'user@test.dev', name: 'user', profileChangedAt: DateTime(2026)); + +final _asset = RemoteAsset( + id: 'asset-1', + name: 'photo.jpg', + ownerId: 'user-1', + checksum: 'checksum-1', + type: AssetType.image, + createdAt: DateTime(2026, 6, 10, 10, 27), + updatedAt: DateTime(2026, 6, 10, 10, 27), + isEdited: false, +); + +void main() { + late ProviderContainer container; + late MockActionService actionService; + late MockAssetService assetService; + + setUpAll(() { + registerFallbackValue(FakeBuildContext()); + registerFallbackValue(_asset); + registerFallbackValue([]); + }); + + setUp(() { + actionService = MockActionService(); + assetService = MockAssetService(); + final userService = MockUserService(); + + when(() => actionService.editDateTime(any(), any())).thenAnswer((_) async => true); + when(() => assetService.watchAsset(any())).thenAnswer((_) => const Stream.empty()); + when(() => assetService.getExif(any())).thenAnswer((_) async => null); + when(() => userService.tryGetMyUser()).thenReturn(_user); + when(() => userService.watchMyUser()).thenAnswer((_) => const Stream.empty()); + + container = ProviderContainer( + overrides: [ + actionServiceProvider.overrideWithValue(actionService), + assetServiceProvider.overrideWithValue(assetService), + downloadServiceProvider.overrideWithValue(MockDownloadService()), + foregroundUploadServiceProvider.overrideWithValue(MockForegroundUploadService()), + currentUserProvider.overrideWith((ref) => CurrentUserProvider(userService)), + ], + ); + addTearDown(container.dispose); + }); + + group('editDateTime', () { + test('refreshes the exif provider when editing from the viewer', () async { + container.read(assetViewerProvider.notifier).setAsset(_asset); + container.listen(assetExifProvider(_asset), (_, __) {}); + await container.read(assetExifProvider(_asset).future); + + final result = await container.read(actionProvider.notifier).editDateTime(ActionSource.viewer, FakeBuildContext()); + + expect(result?.success, isTrue); + await container.read(assetExifProvider(_asset).future); + verify(() => assetService.getExif(_asset)).called(2); + }); + + test('leaves the exif provider cached when editing from the timeline', () async { + container.read(assetViewerProvider.notifier).setAsset(_asset); + container.listen(assetExifProvider(_asset), (_, __) {}); + await container.read(assetExifProvider(_asset).future); + + final result = await container.read(actionProvider.notifier).editDateTime(ActionSource.timeline, FakeBuildContext()); + + expect(result?.success, isTrue); + await container.read(assetExifProvider(_asset).future); + verify(() => assetService.getExif(_asset)).called(1); + }); + + test('does not refresh the exif provider when the edit is cancelled', () async { + when(() => actionService.editDateTime(any(), any())).thenAnswer((_) async => false); + container.read(assetViewerProvider.notifier).setAsset(_asset); + container.listen(assetExifProvider(_asset), (_, __) {}); + await container.read(assetExifProvider(_asset).future); + + final result = await container.read(actionProvider.notifier).editDateTime(ActionSource.viewer, FakeBuildContext()); + + expect(result, isNull); + await container.read(assetExifProvider(_asset).future); + verify(() => assetService.getExif(_asset)).called(1); + }); + }); +}