You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	refactor(mobile): Activities (#5990)
* refactor: autoroutex pushroute * refactor: autoroutex popRoute * refactor: autoroutex navigate and replace * chore: add doc comments for extension methods * refactor: Add LoggerMixin and refactor Album activities to use mixin * refactor: Activity page * chore: activity user from user constructor * fix: update current asset after build method * refactor: tests with similar structure as lib * chore: remove avoid-declaring-call-method rule from dcm analysis * test: fix proper expect order * test: activity_statistics_provider_test * test: activity_provider_test * test: use proper matchers * test: activity_text_field_test & dismissible_activity_test added * test: add http mock to return transparent image * test: download isar core libs during test * test: add widget tags to widget test cases * test: activity_tile_test * build: currentAlbumProvider to generator * movie add / remove like to activity input tile * test: activities_page_test.dart * chore: better error logs * chore: dismissibleactivity as statelesswidget --------- Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
		| @@ -52,7 +52,6 @@ dart_code_metrics: | ||||
|     - avoid-cascade-after-if-null | ||||
|     - avoid-collapsible-if | ||||
|     - avoid-collection-methods-with-unrelated-types | ||||
|     - avoid-declaring-call-method | ||||
|     - avoid-double-slash-imports | ||||
|     - avoid-duplicate-cascades | ||||
|     - avoid-duplicate-patterns | ||||
|   | ||||
							
								
								
									
										3
									
								
								mobile/dart_test.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								mobile/dart_test.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Used to filter out tags from test runs | ||||
| tags: | ||||
|   widget: | ||||
							
								
								
									
										9
									
								
								mobile/lib/constants/errors.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								mobile/lib/constants/errors.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| /// Base class which is used to check if an Exception is a custom exception | ||||
| sealed class ImmichErrors { | ||||
|   const ImmichErrors(); | ||||
| } | ||||
|  | ||||
| class NoResponseDtoError extends ImmichErrors implements Exception { | ||||
|   @override | ||||
|   String toString() => "Response Dto is null"; | ||||
| } | ||||
| @@ -7,6 +7,8 @@ import 'package:logging/logging.dart'; | ||||
| extension LogOnError<T> on AsyncValue<T> { | ||||
|   static final Logger _asyncErrorLogger = Logger("AsyncValue"); | ||||
|  | ||||
|   /// Used to return the [ImmichLoadingIndicator] and [ScaffoldErrorBody] widgets by default on loading | ||||
|   /// and error cases respectively | ||||
|   Widget widgetWhen({ | ||||
|     bool skipLoadingOnRefresh = true, | ||||
|     Widget Function()? onLoading, | ||||
| @@ -28,8 +30,9 @@ extension LogOnError<T> on AsyncValue<T> { | ||||
|     } | ||||
|  | ||||
|     if (hasError && !hasValue) { | ||||
|       _asyncErrorLogger.severe("Error occured", error, stackTrace); | ||||
|       return onError?.call(error, stackTrace) ?? const ScaffoldErrorBody(); | ||||
|       _asyncErrorLogger.severe("$error", error, stackTrace); | ||||
|       return onError?.call(error, stackTrace) ?? | ||||
|           ScaffoldErrorBody(errorMsg: error?.toString()); | ||||
|     } | ||||
|  | ||||
|     return onData(requireValue); | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| extension ContextHelper on BuildContext { | ||||
| @@ -34,21 +33,4 @@ extension ContextHelper on BuildContext { | ||||
|  | ||||
|   // Pop-out from the current context with optional result | ||||
|   void pop<T>([T? result]) => Navigator.of(this).pop(result); | ||||
|  | ||||
|   // Auto-Push new route from the current context | ||||
|   Future<T?> autoPush<T extends Object?>(PageRouteInfo<dynamic> route) => | ||||
|       AutoRouter.of(this).push(route); | ||||
|  | ||||
|   // Auto-Push navigate route from the current context | ||||
|   Future<dynamic> autoNavigate<T extends Object?>( | ||||
|     PageRouteInfo<dynamic> route, | ||||
|   ) => | ||||
|       AutoRouter.of(this).navigate(route); | ||||
|  | ||||
|   // Auto-Push replace route from the current context | ||||
|   Future<T?> autoReplace<T extends Object?>(PageRouteInfo<dynamic> route) => | ||||
|       AutoRouter.of(this).replace(route); | ||||
|  | ||||
|   // Auto-Pop from the current context | ||||
|   Future<bool> autoPop<T>([T? result]) => AutoRouter.of(this).pop(result); | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| extension TimeAgoExtension on DateTime { | ||||
|   /// Displays the time difference of this [DateTime] object to the current time as a [String] | ||||
|   String timeAgo({bool numericDates = true}) { | ||||
|     DateTime date = toLocal(); | ||||
|     final date2 = DateTime.now().toLocal(); | ||||
|     final difference = date2.difference(date); | ||||
|     final now = DateTime.now().toLocal(); | ||||
|     final difference = now.difference(date); | ||||
|  | ||||
|     if (difference.inSeconds < 5) { | ||||
|       return 'Just now'; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| extension TZOffsetExtension on Duration { | ||||
|   /// Formats the duration in the format of ±HH:MM | ||||
|   String formatAsOffset() => | ||||
|       "${isNegative ? '-' : '+'}${inHours.abs().toString().padLeft(2, '0')}:${inMinutes.abs().remainder(60).toString().padLeft(2, '0')}"; | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ extension StringExtension on String { | ||||
| } | ||||
|  | ||||
| extension DurationExtension on String { | ||||
|   /// Parses and returns the string of format HH:MM:SS as a duration object else null | ||||
|   Duration? toDuration() { | ||||
|     try { | ||||
|       final parts = split(':') | ||||
|   | ||||
| @@ -73,14 +73,14 @@ Future<void> initApp() async { | ||||
|   FlutterError.onError = (details) { | ||||
|     FlutterError.presentError(details); | ||||
|     log.severe( | ||||
|       'Catch all error: ${details.toString()} - ${details.exception} - ${details.library} - ${details.context} - ${details.stack}', | ||||
|       'FlutterError - Catch all error: ${details.toString()} - ${details.exception} - ${details.library} - ${details.context} - ${details.stack}', | ||||
|       details, | ||||
|       details.stack, | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   PlatformDispatcher.instance.onError = (error, stack) { | ||||
|     log.severe('Catch all error: ${error.toString()} - $error', error, stack); | ||||
|     log.severe('PlatformDispatcher - Catch all error: $error', error, stack); | ||||
|     return true; | ||||
|   }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										38
									
								
								mobile/lib/mixins/error_logger.mixin.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								mobile/lib/mixins/error_logger.mixin.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
|  | ||||
| typedef AsyncFuture<T> = Future<AsyncValue<T>>; | ||||
|  | ||||
| mixin ErrorLoggerMixin { | ||||
|   abstract final Logger logger; | ||||
|  | ||||
|   /// Returns an AsyncValue<T> if the future is successfully executed | ||||
|   /// Else, logs the error to the overrided logger and returns an AsyncError<> | ||||
|   AsyncFuture<T> guardError<T>( | ||||
|     Future<T> Function() fn, { | ||||
|     Level logLevel = Level.SEVERE, | ||||
|   }) async { | ||||
|     try { | ||||
|       final result = await fn(); | ||||
|       return AsyncData(result); | ||||
|     } catch (error, stackTrace) { | ||||
|       logger.log(logLevel, "$error", error, stackTrace); | ||||
|       return AsyncError(error, stackTrace); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Returns the result of the future if success | ||||
|   /// Else, logs the error and returns the default value | ||||
|   Future<T> logError<T>( | ||||
|     Future<T> Function() fn, { | ||||
|     required T defaultValue, | ||||
|     Level logLevel = Level.SEVERE, | ||||
|   }) async { | ||||
|     try { | ||||
|       return await fn(); | ||||
|     } catch (error, stackTrace) { | ||||
|       logger.log(logLevel, "$error", error, stackTrace); | ||||
|     } | ||||
|     return defaultValue; | ||||
|   } | ||||
| } | ||||
| @@ -46,18 +46,7 @@ class Activity { | ||||
|         type = dto.type == ActivityResponseDtoTypeEnum.comment | ||||
|             ? ActivityType.comment | ||||
|             : ActivityType.like, | ||||
|         user = User( | ||||
|           email: dto.user.email, | ||||
|           name: dto.user.name, | ||||
|           profileImagePath: dto.user.profileImagePath, | ||||
|           id: dto.user.id, | ||||
|           // Placeholder values | ||||
|           isAdmin: false, | ||||
|           updatedAt: DateTime.now(), | ||||
|           isPartnerSharedBy: false, | ||||
|           isPartnerSharedWith: false, | ||||
|           memoryEnabled: false, | ||||
|         ); | ||||
|         user = User.fromSimpleUserDto(dto.user); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
| @@ -65,11 +54,10 @@ class Activity { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|   bool operator ==(covariant Activity other) { | ||||
|     if (identical(this, other)) return true; | ||||
|  | ||||
|     return other is Activity && | ||||
|         other.id == id && | ||||
|     return other.id == id && | ||||
|         other.assetId == assetId && | ||||
|         other.comment == comment && | ||||
|         other.createdAt == createdAt && | ||||
|   | ||||
| @@ -1,134 +1,67 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/activities/models/activity.model.dart'; | ||||
| import 'package:immich_mobile/modules/activities/services/activity.service.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| class ActivityNotifier extends StateNotifier<AsyncValue<List<Activity>>> { | ||||
|   final Ref _ref; | ||||
|   final ActivityService _activityService; | ||||
|   final String albumId; | ||||
|   final String? assetId; | ||||
| part 'activity.provider.g.dart'; | ||||
|  | ||||
|   ActivityNotifier( | ||||
|     this._ref, | ||||
|     this._activityService, | ||||
|     this.albumId, | ||||
|     this.assetId, | ||||
|   ) : super( | ||||
|           const AsyncData([]), | ||||
|         ) { | ||||
|     fetchActivity(); | ||||
|   } | ||||
|  | ||||
|   Future<void> fetchActivity() async { | ||||
|     state = const AsyncLoading(); | ||||
|     state = await AsyncValue.guard( | ||||
|       () => _activityService.getAllActivities(albumId, assetId), | ||||
|     ); | ||||
| /// Maintains the current list of all activities for <share-album-id, asset> | ||||
| @riverpod | ||||
| class AlbumActivity extends _$AlbumActivity { | ||||
|   @override | ||||
|   Future<List<Activity>> build(String albumId, [String? assetId]) async { | ||||
|     return ref | ||||
|         .watch(activityServiceProvider) | ||||
|         .getAllActivities(albumId, assetId: assetId); | ||||
|   } | ||||
|  | ||||
|   Future<void> removeActivity(String id) async { | ||||
|     final activities = state.asData?.value ?? []; | ||||
|     if (await _activityService.removeActivity(id)) { | ||||
|     if (await ref.watch(activityServiceProvider).removeActivity(id)) { | ||||
|       final activities = state.valueOrNull ?? []; | ||||
|       final removedActivity = activities.firstWhere((a) => a.id == id); | ||||
|       activities.remove(removedActivity); | ||||
|       state = AsyncData(activities); | ||||
|       // Decrement activity count only for comments | ||||
|       if (removedActivity.type == ActivityType.comment) { | ||||
|         _ref | ||||
|             .read( | ||||
|               activityStatisticsStateProvider( | ||||
|                 (albumId: albumId, assetId: assetId), | ||||
|               ).notifier, | ||||
|             ) | ||||
|         ref | ||||
|             .watch(activityStatisticsProvider(albumId, assetId).notifier) | ||||
|             .removeActivity(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> addComment(String comment) async { | ||||
|     final activity = await _activityService.addActivity( | ||||
|       albumId, | ||||
|       ActivityType.comment, | ||||
|       assetId: assetId, | ||||
|       comment: comment, | ||||
|     ); | ||||
|  | ||||
|     if (activity != null) { | ||||
|   Future<void> addLike() async { | ||||
|     final activity = await ref | ||||
|         .watch(activityServiceProvider) | ||||
|         .addActivity(albumId, ActivityType.like, assetId: assetId); | ||||
|     if (activity.hasValue) { | ||||
|       final activities = state.asData?.value ?? []; | ||||
|       state = AsyncData([...activities, activity]); | ||||
|       _ref | ||||
|           .read( | ||||
|             activityStatisticsStateProvider( | ||||
|               (albumId: albumId, assetId: assetId), | ||||
|             ).notifier, | ||||
|           ) | ||||
|       state = AsyncData([...activities, activity.requireValue]); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> addComment(String comment) async { | ||||
|     final activity = await ref.watch(activityServiceProvider).addActivity( | ||||
|           albumId, | ||||
|           ActivityType.comment, | ||||
|           assetId: assetId, | ||||
|           comment: comment, | ||||
|         ); | ||||
|  | ||||
|     if (activity.hasValue) { | ||||
|       final activities = state.valueOrNull ?? []; | ||||
|       state = AsyncData([...activities, activity.requireValue]); | ||||
|       ref | ||||
|           .watch(activityStatisticsProvider(albumId, assetId).notifier) | ||||
|           .addActivity(); | ||||
|       // The previous addActivity call would increase the count of an asset if assetId != null | ||||
|       // To also increase the activity count of the album, calling it once again with assetId set to null | ||||
|       if (assetId != null) { | ||||
|         // Add a count to the current album's provider as well | ||||
|         _ref | ||||
|             .read( | ||||
|               activityStatisticsStateProvider( | ||||
|                 (albumId: albumId, assetId: null), | ||||
|               ).notifier, | ||||
|             ) | ||||
|             .addActivity(); | ||||
|         ref.watch(activityStatisticsProvider(albumId).notifier).addActivity(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> addLike() async { | ||||
|     final activity = await _activityService | ||||
|         .addActivity(albumId, ActivityType.like, assetId: assetId); | ||||
|     if (activity != null) { | ||||
|       final activities = state.asData?.value ?? []; | ||||
|       state = AsyncData([...activities, activity]); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ActivityStatisticsNotifier extends StateNotifier<int> { | ||||
|   final String albumId; | ||||
|   final String? assetId; | ||||
|   final ActivityService _activityService; | ||||
|   ActivityStatisticsNotifier(this._activityService, this.albumId, this.assetId) | ||||
|       : super(0) { | ||||
|     fetchStatistics(); | ||||
|   } | ||||
|  | ||||
|   Future<void> fetchStatistics() async { | ||||
|     final count = | ||||
|         await _activityService.getStatistics(albumId, assetId: assetId); | ||||
|     if (mounted) { | ||||
|       state = count; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> addActivity() async { | ||||
|     state = state + 1; | ||||
|   } | ||||
|  | ||||
|   Future<void> removeActivity() async { | ||||
|     state = state - 1; | ||||
|   } | ||||
| } | ||||
|  | ||||
| typedef ActivityParams = ({String albumId, String? assetId}); | ||||
|  | ||||
| final activityStateProvider = StateNotifierProvider.autoDispose | ||||
|     .family<ActivityNotifier, AsyncValue<List<Activity>>, ActivityParams>( | ||||
|         (ref, args) { | ||||
|   return ActivityNotifier( | ||||
|     ref, | ||||
|     ref.watch(activityServiceProvider), | ||||
|     args.albumId, | ||||
|     args.assetId, | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| final activityStatisticsStateProvider = StateNotifierProvider.autoDispose | ||||
|     .family<ActivityStatisticsNotifier, int, ActivityParams>((ref, args) { | ||||
|   return ActivityStatisticsNotifier( | ||||
|     ref.watch(activityServiceProvider), | ||||
|     args.albumId, | ||||
|     args.assetId, | ||||
|   ); | ||||
| }); | ||||
| /// Mock class for testing | ||||
| abstract class AlbumActivityInternal extends _$AlbumActivity {} | ||||
|   | ||||
							
								
								
									
										209
									
								
								mobile/lib/modules/activities/providers/activity.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								mobile/lib/modules/activities/providers/activity.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'activity.provider.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| String _$albumActivityHash() => r'3b0d7acee4d41c84b3f220784c3b904c83f836e6'; | ||||
| 
 | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   _SystemHash._(); | ||||
| 
 | ||||
|   static int combine(int hash, int value) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + value); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | ||||
|     return hash ^ (hash >> 6); | ||||
|   } | ||||
| 
 | ||||
|   static int finish(int hash) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = hash ^ (hash >> 11); | ||||
|     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| abstract class _$AlbumActivity | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<List<Activity>> { | ||||
|   late final String albumId; | ||||
|   late final String? assetId; | ||||
| 
 | ||||
|   Future<List<Activity>> build( | ||||
|     String albumId, [ | ||||
|     String? assetId, | ||||
|   ]); | ||||
| } | ||||
| 
 | ||||
| /// Maintains the current list of all activities for <share-album-id, asset> | ||||
| /// | ||||
| /// Copied from [AlbumActivity]. | ||||
| @ProviderFor(AlbumActivity) | ||||
| const albumActivityProvider = AlbumActivityFamily(); | ||||
| 
 | ||||
| /// Maintains the current list of all activities for <share-album-id, asset> | ||||
| /// | ||||
| /// Copied from [AlbumActivity]. | ||||
| class AlbumActivityFamily extends Family<AsyncValue<List<Activity>>> { | ||||
|   /// Maintains the current list of all activities for <share-album-id, asset> | ||||
|   /// | ||||
|   /// Copied from [AlbumActivity]. | ||||
|   const AlbumActivityFamily(); | ||||
| 
 | ||||
|   /// Maintains the current list of all activities for <share-album-id, asset> | ||||
|   /// | ||||
|   /// Copied from [AlbumActivity]. | ||||
|   AlbumActivityProvider call( | ||||
|     String albumId, [ | ||||
|     String? assetId, | ||||
|   ]) { | ||||
|     return AlbumActivityProvider( | ||||
|       albumId, | ||||
|       assetId, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   AlbumActivityProvider getProviderOverride( | ||||
|     covariant AlbumActivityProvider provider, | ||||
|   ) { | ||||
|     return call( | ||||
|       provider.albumId, | ||||
|       provider.assetId, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
| 
 | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
| 
 | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
| 
 | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
| 
 | ||||
|   @override | ||||
|   String? get name => r'albumActivityProvider'; | ||||
| } | ||||
| 
 | ||||
| /// Maintains the current list of all activities for <share-album-id, asset> | ||||
| /// | ||||
| /// Copied from [AlbumActivity]. | ||||
| class AlbumActivityProvider extends AutoDisposeAsyncNotifierProviderImpl< | ||||
|     AlbumActivity, List<Activity>> { | ||||
|   /// Maintains the current list of all activities for <share-album-id, asset> | ||||
|   /// | ||||
|   /// Copied from [AlbumActivity]. | ||||
|   AlbumActivityProvider( | ||||
|     String albumId, [ | ||||
|     String? assetId, | ||||
|   ]) : this._internal( | ||||
|           () => AlbumActivity() | ||||
|             ..albumId = albumId | ||||
|             ..assetId = assetId, | ||||
|           from: albumActivityProvider, | ||||
|           name: r'albumActivityProvider', | ||||
|           debugGetCreateSourceHash: | ||||
|               const bool.fromEnvironment('dart.vm.product') | ||||
|                   ? null | ||||
|                   : _$albumActivityHash, | ||||
|           dependencies: AlbumActivityFamily._dependencies, | ||||
|           allTransitiveDependencies: | ||||
|               AlbumActivityFamily._allTransitiveDependencies, | ||||
|           albumId: albumId, | ||||
|           assetId: assetId, | ||||
|         ); | ||||
| 
 | ||||
|   AlbumActivityProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.albumId, | ||||
|     required this.assetId, | ||||
|   }) : super.internal(); | ||||
| 
 | ||||
|   final String albumId; | ||||
|   final String? assetId; | ||||
| 
 | ||||
|   @override | ||||
|   Future<List<Activity>> runNotifierBuild( | ||||
|     covariant AlbumActivity notifier, | ||||
|   ) { | ||||
|     return notifier.build( | ||||
|       albumId, | ||||
|       assetId, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Override overrideWith(AlbumActivity Function() create) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: AlbumActivityProvider._internal( | ||||
|         () => create() | ||||
|           ..albumId = albumId | ||||
|           ..assetId = assetId, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         albumId: albumId, | ||||
|         assetId: assetId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   AutoDisposeAsyncNotifierProviderElement<AlbumActivity, List<Activity>> | ||||
|       createElement() { | ||||
|     return _AlbumActivityProviderElement(this); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is AlbumActivityProvider && | ||||
|         other.albumId == albumId && | ||||
|         other.assetId == assetId; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, albumId.hashCode); | ||||
|     hash = _SystemHash.combine(hash, assetId.hashCode); | ||||
| 
 | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| mixin AlbumActivityRef on AutoDisposeAsyncNotifierProviderRef<List<Activity>> { | ||||
|   /// The parameter `albumId` of this provider. | ||||
|   String get albumId; | ||||
| 
 | ||||
|   /// The parameter `assetId` of this provider. | ||||
|   String? get assetId; | ||||
| } | ||||
| 
 | ||||
| class _AlbumActivityProviderElement | ||||
|     extends AutoDisposeAsyncNotifierProviderElement<AlbumActivity, | ||||
|         List<Activity>> with AlbumActivityRef { | ||||
|   _AlbumActivityProviderElement(super.provider); | ||||
| 
 | ||||
|   @override | ||||
|   String get albumId => (origin as AlbumActivityProvider).albumId; | ||||
|   @override | ||||
|   String? get assetId => (origin as AlbumActivityProvider).assetId; | ||||
| } | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member | ||||
| @@ -0,0 +1,9 @@ | ||||
| import 'package:immich_mobile/modules/activities/services/activity.service.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| part 'activity_service.provider.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| ActivityService activityService(ActivityServiceRef ref) => | ||||
|     ActivityService(ref.watch(apiServiceProvider)); | ||||
							
								
								
									
										25
									
								
								mobile/lib/modules/activities/providers/activity_service.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								mobile/lib/modules/activities/providers/activity_service.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'activity_service.provider.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| String _$activityServiceHash() => r'5dd4955d14f5bf01c00d7f8750d07e7ace7cc4b0'; | ||||
| 
 | ||||
| /// See also [activityService]. | ||||
| @ProviderFor(activityService) | ||||
| final activityServiceProvider = AutoDisposeProvider<ActivityService>.internal( | ||||
|   activityService, | ||||
|   name: r'activityServiceProvider', | ||||
|   debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') | ||||
|       ? null | ||||
|       : _$activityServiceHash, | ||||
|   dependencies: null, | ||||
|   allTransitiveDependencies: null, | ||||
| ); | ||||
| 
 | ||||
| typedef ActivityServiceRef = AutoDisposeProviderRef<ActivityService>; | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member | ||||
| @@ -0,0 +1,24 @@ | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| part 'activity_statistics.provider.g.dart'; | ||||
|  | ||||
| /// Maintains the current number of comments by <shared-album, asset> | ||||
| @riverpod | ||||
| class ActivityStatistics extends _$ActivityStatistics { | ||||
|   @override | ||||
|   int build(String albumId, [String? assetId]) { | ||||
|     ref | ||||
|         .watch(activityServiceProvider) | ||||
|         .getStatistics(albumId, assetId: assetId) | ||||
|         .then((comments) => state = comments); | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   void addActivity() => state = state + 1; | ||||
|  | ||||
|   void removeActivity() => state = state - 1; | ||||
| } | ||||
|  | ||||
| /// Mock class for testing | ||||
| abstract class ActivityStatisticsInternal extends _$ActivityStatistics {} | ||||
							
								
								
									
										208
									
								
								mobile/lib/modules/activities/providers/activity_statistics.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								mobile/lib/modules/activities/providers/activity_statistics.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'activity_statistics.provider.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| String _$activityStatisticsHash() => | ||||
|     r'a5f7bbee1891c33b72919a34e632ca9ef9cd8dbf'; | ||||
| 
 | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   _SystemHash._(); | ||||
| 
 | ||||
|   static int combine(int hash, int value) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + value); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | ||||
|     return hash ^ (hash >> 6); | ||||
|   } | ||||
| 
 | ||||
|   static int finish(int hash) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = hash ^ (hash >> 11); | ||||
|     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| abstract class _$ActivityStatistics extends BuildlessAutoDisposeNotifier<int> { | ||||
|   late final String albumId; | ||||
|   late final String? assetId; | ||||
| 
 | ||||
|   int build( | ||||
|     String albumId, [ | ||||
|     String? assetId, | ||||
|   ]); | ||||
| } | ||||
| 
 | ||||
| /// Maintains the current number of comments by <shared-album, asset> | ||||
| /// | ||||
| /// Copied from [ActivityStatistics]. | ||||
| @ProviderFor(ActivityStatistics) | ||||
| const activityStatisticsProvider = ActivityStatisticsFamily(); | ||||
| 
 | ||||
| /// Maintains the current number of comments by <shared-album, asset> | ||||
| /// | ||||
| /// Copied from [ActivityStatistics]. | ||||
| class ActivityStatisticsFamily extends Family<int> { | ||||
|   /// Maintains the current number of comments by <shared-album, asset> | ||||
|   /// | ||||
|   /// Copied from [ActivityStatistics]. | ||||
|   const ActivityStatisticsFamily(); | ||||
| 
 | ||||
|   /// Maintains the current number of comments by <shared-album, asset> | ||||
|   /// | ||||
|   /// Copied from [ActivityStatistics]. | ||||
|   ActivityStatisticsProvider call( | ||||
|     String albumId, [ | ||||
|     String? assetId, | ||||
|   ]) { | ||||
|     return ActivityStatisticsProvider( | ||||
|       albumId, | ||||
|       assetId, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   ActivityStatisticsProvider getProviderOverride( | ||||
|     covariant ActivityStatisticsProvider provider, | ||||
|   ) { | ||||
|     return call( | ||||
|       provider.albumId, | ||||
|       provider.assetId, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
| 
 | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
| 
 | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
| 
 | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
| 
 | ||||
|   @override | ||||
|   String? get name => r'activityStatisticsProvider'; | ||||
| } | ||||
| 
 | ||||
| /// Maintains the current number of comments by <shared-album, asset> | ||||
| /// | ||||
| /// Copied from [ActivityStatistics]. | ||||
| class ActivityStatisticsProvider | ||||
|     extends AutoDisposeNotifierProviderImpl<ActivityStatistics, int> { | ||||
|   /// Maintains the current number of comments by <shared-album, asset> | ||||
|   /// | ||||
|   /// Copied from [ActivityStatistics]. | ||||
|   ActivityStatisticsProvider( | ||||
|     String albumId, [ | ||||
|     String? assetId, | ||||
|   ]) : this._internal( | ||||
|           () => ActivityStatistics() | ||||
|             ..albumId = albumId | ||||
|             ..assetId = assetId, | ||||
|           from: activityStatisticsProvider, | ||||
|           name: r'activityStatisticsProvider', | ||||
|           debugGetCreateSourceHash: | ||||
|               const bool.fromEnvironment('dart.vm.product') | ||||
|                   ? null | ||||
|                   : _$activityStatisticsHash, | ||||
|           dependencies: ActivityStatisticsFamily._dependencies, | ||||
|           allTransitiveDependencies: | ||||
|               ActivityStatisticsFamily._allTransitiveDependencies, | ||||
|           albumId: albumId, | ||||
|           assetId: assetId, | ||||
|         ); | ||||
| 
 | ||||
|   ActivityStatisticsProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.albumId, | ||||
|     required this.assetId, | ||||
|   }) : super.internal(); | ||||
| 
 | ||||
|   final String albumId; | ||||
|   final String? assetId; | ||||
| 
 | ||||
|   @override | ||||
|   int runNotifierBuild( | ||||
|     covariant ActivityStatistics notifier, | ||||
|   ) { | ||||
|     return notifier.build( | ||||
|       albumId, | ||||
|       assetId, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Override overrideWith(ActivityStatistics Function() create) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: ActivityStatisticsProvider._internal( | ||||
|         () => create() | ||||
|           ..albumId = albumId | ||||
|           ..assetId = assetId, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         albumId: albumId, | ||||
|         assetId: assetId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   AutoDisposeNotifierProviderElement<ActivityStatistics, int> createElement() { | ||||
|     return _ActivityStatisticsProviderElement(this); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is ActivityStatisticsProvider && | ||||
|         other.albumId == albumId && | ||||
|         other.assetId == assetId; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, albumId.hashCode); | ||||
|     hash = _SystemHash.combine(hash, assetId.hashCode); | ||||
| 
 | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| mixin ActivityStatisticsRef on AutoDisposeNotifierProviderRef<int> { | ||||
|   /// The parameter `albumId` of this provider. | ||||
|   String get albumId; | ||||
| 
 | ||||
|   /// The parameter `assetId` of this provider. | ||||
|   String? get assetId; | ||||
| } | ||||
| 
 | ||||
| class _ActivityStatisticsProviderElement | ||||
|     extends AutoDisposeNotifierProviderElement<ActivityStatistics, int> | ||||
|     with ActivityStatisticsRef { | ||||
|   _ActivityStatisticsProviderElement(super.provider); | ||||
| 
 | ||||
|   @override | ||||
|   String get albumId => (origin as ActivityStatisticsProvider).albumId; | ||||
|   @override | ||||
|   String? get assetId => (origin as ActivityStatisticsProvider).assetId; | ||||
| } | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member | ||||
| @@ -1,67 +1,60 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/errors.dart'; | ||||
| import 'package:immich_mobile/mixins/error_logger.mixin.dart'; | ||||
| import 'package:immich_mobile/modules/activities/models/activity.model.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| final activityServiceProvider = | ||||
|     Provider((ref) => ActivityService(ref.watch(apiServiceProvider))); | ||||
|  | ||||
| class ActivityService { | ||||
| class ActivityService with ErrorLoggerMixin { | ||||
|   final ApiService _apiService; | ||||
|   final Logger _log = Logger("ActivityService"); | ||||
|  | ||||
|   @override | ||||
|   final Logger logger = Logger("ActivityService"); | ||||
|  | ||||
|   ActivityService(this._apiService); | ||||
|  | ||||
|   Future<List<Activity>> getAllActivities( | ||||
|     String albumId, | ||||
|     String albumId, { | ||||
|     String? assetId, | ||||
|   ) async { | ||||
|     try { | ||||
|       final list = await _apiService.activityApi | ||||
|           .getActivities(albumId, assetId: assetId); | ||||
|       return list != null ? list.map(Activity.fromDto).toList() : []; | ||||
|     } catch (e) { | ||||
|       _log.severe( | ||||
|         "failed to fetch activities for albumId - $albumId; assetId - $assetId -> $e", | ||||
|       ); | ||||
|       rethrow; | ||||
|     } | ||||
|   }) async { | ||||
|     return logError( | ||||
|       () async { | ||||
|         final list = await _apiService.activityApi | ||||
|             .getActivities(albumId, assetId: assetId); | ||||
|         return list != null ? list.map(Activity.fromDto).toList() : []; | ||||
|       }, | ||||
|       defaultValue: [], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<int> getStatistics(String albumId, {String? assetId}) async { | ||||
|     try { | ||||
|       final dto = await _apiService.activityApi | ||||
|           .getActivityStatistics(albumId, assetId: assetId); | ||||
|       return dto?.comments ?? 0; | ||||
|     } catch (e) { | ||||
|       _log.severe( | ||||
|         "failed to fetch activity statistics for albumId - $albumId; assetId - $assetId -> $e", | ||||
|       ); | ||||
|     } | ||||
|     return 0; | ||||
|     return logError( | ||||
|       () async { | ||||
|         final dto = await _apiService.activityApi | ||||
|             .getActivityStatistics(albumId, assetId: assetId); | ||||
|         return dto?.comments ?? 0; | ||||
|       }, | ||||
|       defaultValue: 0, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<bool> removeActivity(String id) async { | ||||
|     try { | ||||
|       await _apiService.activityApi.deleteActivity(id); | ||||
|       return true; | ||||
|     } catch (e) { | ||||
|       _log.severe( | ||||
|         "failed to remove activity id - $id -> $e", | ||||
|       ); | ||||
|     } | ||||
|     return false; | ||||
|     return logError( | ||||
|       () async { | ||||
|         await _apiService.activityApi.deleteActivity(id); | ||||
|         return true; | ||||
|       }, | ||||
|       defaultValue: false, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<Activity?> addActivity( | ||||
|   AsyncFuture<Activity> addActivity( | ||||
|     String albumId, | ||||
|     ActivityType type, { | ||||
|     String? assetId, | ||||
|     String? comment, | ||||
|   }) async { | ||||
|     try { | ||||
|     return guardError(() async { | ||||
|       final dto = await _apiService.activityApi.createActivity( | ||||
|         ActivityCreateDto( | ||||
|           albumId: albumId, | ||||
| @@ -75,11 +68,7 @@ class ActivityService { | ||||
|       if (dto != null) { | ||||
|         return Activity.fromDto(dto); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       _log.severe( | ||||
|         "failed to add activity for albumId - $albumId; assetId - $assetId -> $e", | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|       throw NoResponseDtoError(); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -8,236 +6,51 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/activities/models/activity.model.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
| import 'package:immich_mobile/extensions/datetime_extensions.dart'; | ||||
| import 'package:immich_mobile/utils/image_url_builder.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/activity_text_field.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/dismissible_activity.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/current_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/user.provider.dart'; | ||||
|  | ||||
| class ActivitiesPage extends HookConsumerWidget { | ||||
|   final String albumId; | ||||
|   final String? assetId; | ||||
|   final bool withAssetThumbs; | ||||
|   final String appBarTitle; | ||||
|   final bool isOwner; | ||||
|   final bool isReadOnly; | ||||
|   const ActivitiesPage( | ||||
|     this.albumId, { | ||||
|     this.appBarTitle = "", | ||||
|     this.assetId, | ||||
|     this.withAssetThumbs = true, | ||||
|     this.isOwner = false, | ||||
|     this.isReadOnly = false, | ||||
|   const ActivitiesPage({ | ||||
|     super.key, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final provider = | ||||
|         activityStateProvider((albumId: albumId, assetId: assetId)); | ||||
|     final activities = ref.watch(provider); | ||||
|     final inputController = useTextEditingController(); | ||||
|     final inputFocusNode = useFocusNode(); | ||||
|     // Album has to be set in the provider before reaching this page | ||||
|     final album = ref.watch(currentAlbumProvider)!; | ||||
|     final asset = ref.watch(currentAssetProvider); | ||||
|     final user = ref.watch(currentUserProvider); | ||||
|  | ||||
|     final activityNotifier = ref | ||||
|         .read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); | ||||
|     final activities = | ||||
|         ref.watch(albumActivityProvider(album.remoteId!, asset?.remoteId)); | ||||
|  | ||||
|     final listViewScrollController = useScrollController(); | ||||
|     final currentUser = Store.tryGet(StoreKey.currentUser); | ||||
|  | ||||
|     useEffect( | ||||
|       () { | ||||
|         inputFocusNode.requestFocus(); | ||||
|         return null; | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     buildTitleWithTimestamp(Activity activity, {bool leftAlign = true}) { | ||||
|       final textColor = context.isDarkTheme ? Colors.white : Colors.black; | ||||
|       final textStyle = context.textTheme.bodyMedium | ||||
|           ?.copyWith(color: textColor.withOpacity(0.6)); | ||||
|  | ||||
|       return Row( | ||||
|         mainAxisAlignment: leftAlign | ||||
|             ? MainAxisAlignment.start | ||||
|             : MainAxisAlignment.spaceBetween, | ||||
|         mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max, | ||||
|         children: [ | ||||
|           Text( | ||||
|             activity.user.name, | ||||
|             style: textStyle, | ||||
|             overflow: TextOverflow.ellipsis, | ||||
|           ), | ||||
|           if (leftAlign) | ||||
|             Text( | ||||
|               " • ", | ||||
|               style: textStyle, | ||||
|             ), | ||||
|           Expanded( | ||||
|             child: Text( | ||||
|               activity.createdAt.copyWith().timeAgo(), | ||||
|               style: textStyle, | ||||
|               overflow: TextOverflow.ellipsis, | ||||
|               textAlign: leftAlign ? TextAlign.left : TextAlign.right, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     buildAssetThumbnail(Activity activity) { | ||||
|       return withAssetThumbs && activity.assetId != null | ||||
|           ? Container( | ||||
|               width: 40, | ||||
|               height: 30, | ||||
|               decoration: BoxDecoration( | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(4)), | ||||
|                 image: DecorationImage( | ||||
|                   image: CachedNetworkImageProvider( | ||||
|                     getThumbnailUrlForRemoteId( | ||||
|                       activity.assetId!, | ||||
|                     ), | ||||
|                     cacheKey: getThumbnailCacheKeyForRemoteId( | ||||
|                       activity.assetId!, | ||||
|                     ), | ||||
|                     headers: { | ||||
|                       "Authorization": | ||||
|                           'Bearer ${Store.get(StoreKey.accessToken)}', | ||||
|                     }, | ||||
|                   ), | ||||
|                   fit: BoxFit.cover, | ||||
|                 ), | ||||
|               ), | ||||
|               child: const SizedBox.shrink(), | ||||
|             ) | ||||
|           : null; | ||||
|     } | ||||
|  | ||||
|     buildTextField(String? likedId) { | ||||
|       final liked = likedId != null; | ||||
|       return Padding( | ||||
|         padding: const EdgeInsets.only(bottom: 10), | ||||
|         child: TextField( | ||||
|           controller: inputController, | ||||
|           enabled: !isReadOnly, | ||||
|           focusNode: inputFocusNode, | ||||
|           textInputAction: TextInputAction.send, | ||||
|           autofocus: false, | ||||
|           decoration: InputDecoration( | ||||
|             border: InputBorder.none, | ||||
|             focusedBorder: InputBorder.none, | ||||
|             prefixIcon: currentUser != null | ||||
|                 ? Padding( | ||||
|                     padding: const EdgeInsets.symmetric(horizontal: 15), | ||||
|                     child: UserCircleAvatar( | ||||
|                       user: currentUser, | ||||
|                       size: 30, | ||||
|                       radius: 15, | ||||
|                     ), | ||||
|                   ) | ||||
|                 : null, | ||||
|             suffixIcon: Padding( | ||||
|               padding: const EdgeInsets.only(right: 10), | ||||
|               child: IconButton( | ||||
|                 icon: Icon( | ||||
|                   liked | ||||
|                       ? Icons.favorite_rounded | ||||
|                       : Icons.favorite_border_rounded, | ||||
|                 ), | ||||
|                 onPressed: () async { | ||||
|                   liked | ||||
|                       ? await ref | ||||
|                           .read(provider.notifier) | ||||
|                           .removeActivity(likedId) | ||||
|                       : await ref.read(provider.notifier).addLike(); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|             suffixIconColor: liked ? Colors.red[700] : null, | ||||
|             hintText: isReadOnly | ||||
|                 ? 'shared_album_activities_input_disable'.tr() | ||||
|                 : 'shared_album_activities_input_hint'.tr(), | ||||
|             hintStyle: TextStyle( | ||||
|               fontWeight: FontWeight.normal, | ||||
|               fontSize: 14, | ||||
|               color: Colors.grey[600], | ||||
|             ), | ||||
|           ), | ||||
|           onEditingComplete: () async { | ||||
|             await ref.read(provider.notifier).addComment(inputController.text); | ||||
|             inputController.clear(); | ||||
|             inputFocusNode.unfocus(); | ||||
|             listViewScrollController.animateTo( | ||||
|               listViewScrollController.position.maxScrollExtent, | ||||
|               duration: const Duration(milliseconds: 800), | ||||
|               curve: Curves.fastOutSlowIn, | ||||
|             ); | ||||
|           }, | ||||
|           onTapOutside: (_) => inputFocusNode.unfocus(), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     getDismissibleWidget( | ||||
|       Widget widget, | ||||
|       Activity activity, | ||||
|       bool canDelete, | ||||
|     ) { | ||||
|       return Dismissible( | ||||
|         key: Key(activity.id), | ||||
|         dismissThresholds: const { | ||||
|           DismissDirection.horizontal: 0.7, | ||||
|         }, | ||||
|         direction: DismissDirection.horizontal, | ||||
|         confirmDismiss: (direction) => canDelete | ||||
|             ? showDialog( | ||||
|                 context: context, | ||||
|                 builder: (context) => ConfirmDialog( | ||||
|                   onOk: () {}, | ||||
|                   title: "shared_album_activity_remove_title", | ||||
|                   content: "shared_album_activity_remove_content", | ||||
|                   ok: "delete_dialog_ok", | ||||
|                 ), | ||||
|               ) | ||||
|             : Future.value(false), | ||||
|         onDismissed: (direction) async => | ||||
|             await ref.read(provider.notifier).removeActivity(activity.id), | ||||
|         background: Container( | ||||
|           color: canDelete ? Colors.red[400] : Colors.grey[600], | ||||
|           alignment: AlignmentDirectional.centerStart, | ||||
|           child: canDelete | ||||
|               ? const Padding( | ||||
|                   padding: EdgeInsets.all(15), | ||||
|                   child: Icon( | ||||
|                     Icons.delete_sweep_rounded, | ||||
|                     color: Colors.black, | ||||
|                   ), | ||||
|                 ) | ||||
|               : null, | ||||
|         ), | ||||
|         secondaryBackground: Container( | ||||
|           color: canDelete ? Colors.red[400] : Colors.grey[600], | ||||
|           alignment: AlignmentDirectional.centerEnd, | ||||
|           child: canDelete | ||||
|               ? const Padding( | ||||
|                   padding: EdgeInsets.all(15), | ||||
|                   child: Icon( | ||||
|                     Icons.delete_sweep_rounded, | ||||
|                     color: Colors.black, | ||||
|                   ), | ||||
|                 ) | ||||
|               : null, | ||||
|         ), | ||||
|         child: widget, | ||||
|     Future<void> onAddComment(String comment) async { | ||||
|       await activityNotifier.addComment(comment); | ||||
|       // Scroll to the end of the list to show the newly added activity | ||||
|       listViewScrollController.animateTo( | ||||
|         listViewScrollController.position.maxScrollExtent + 200, | ||||
|         duration: const Duration(milliseconds: 600), | ||||
|         curve: Curves.fastOutSlowIn, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar(title: Text(appBarTitle)), | ||||
|       appBar: AppBar(title: asset == null ? Text(album.name) : null), | ||||
|       body: activities.widgetWhen( | ||||
|         onData: (data) { | ||||
|           final liked = data.firstWhereOrNull( | ||||
|             (a) => | ||||
|                 a.type == ActivityType.like && | ||||
|                 a.user.id == currentUser?.id && | ||||
|                 a.assetId == assetId, | ||||
|                 a.user.id == user?.id && | ||||
|                 a.assetId == asset?.remoteId, | ||||
|           ); | ||||
|  | ||||
|           return SafeArea( | ||||
| @@ -245,9 +58,10 @@ class ActivitiesPage extends HookConsumerWidget { | ||||
|               children: [ | ||||
|                 ListView.builder( | ||||
|                   controller: listViewScrollController, | ||||
|                   // +1 to display an additional over-scroll space after the last element | ||||
|                   itemCount: data.length + 1, | ||||
|                   itemBuilder: (context, index) { | ||||
|                     // Vertical gap after the last element | ||||
|                     // Additional vertical gap after the last element | ||||
|                     if (index == data.length) { | ||||
|                       return const SizedBox( | ||||
|                         height: 80, | ||||
| @@ -255,45 +69,19 @@ class ActivitiesPage extends HookConsumerWidget { | ||||
|                     } | ||||
|  | ||||
|                     final activity = data[index]; | ||||
|                     final canDelete = | ||||
|                         activity.user.id == currentUser?.id || isOwner; | ||||
|                     final canDelete = activity.user.id == user?.id || | ||||
|                         album.ownerId == user?.id; | ||||
|  | ||||
|                     return Padding( | ||||
|                       padding: const EdgeInsets.all(5), | ||||
|                       child: activity.type == ActivityType.comment | ||||
|                           ? getDismissibleWidget( | ||||
|                               ListTile( | ||||
|                                 minVerticalPadding: 15, | ||||
|                                 leading: UserCircleAvatar(user: activity.user), | ||||
|                                 title: buildTitleWithTimestamp( | ||||
|                                   activity, | ||||
|                                   leftAlign: withAssetThumbs && | ||||
|                                       activity.assetId != null, | ||||
|                                 ), | ||||
|                                 titleAlignment: ListTileTitleAlignment.top, | ||||
|                                 trailing: buildAssetThumbnail(activity), | ||||
|                                 subtitle: Text(activity.comment!), | ||||
|                               ), | ||||
|                               activity, | ||||
|                               canDelete, | ||||
|                             ) | ||||
|                           : getDismissibleWidget( | ||||
|                               ListTile( | ||||
|                                 minVerticalPadding: 15, | ||||
|                                 leading: Container( | ||||
|                                   width: 44, | ||||
|                                   alignment: Alignment.center, | ||||
|                                   child: Icon( | ||||
|                                     Icons.favorite_rounded, | ||||
|                                     color: Colors.red[700], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 title: buildTitleWithTimestamp(activity), | ||||
|                                 trailing: buildAssetThumbnail(activity), | ||||
|                               ), | ||||
|                               activity, | ||||
|                               canDelete, | ||||
|                             ), | ||||
|                       child: DismissibleActivity( | ||||
|                         activity.id, | ||||
|                         ActivityTile(activity), | ||||
|                         onDismiss: canDelete | ||||
|                             ? (activityId) async => await activityNotifier | ||||
|                                 .removeActivity(activity.id) | ||||
|                             : null, | ||||
|                       ), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
| @@ -301,7 +89,11 @@ class ActivitiesPage extends HookConsumerWidget { | ||||
|                   alignment: Alignment.bottomCenter, | ||||
|                   child: Container( | ||||
|                     color: context.scaffoldBackgroundColor, | ||||
|                     child: buildTextField(liked?.id), | ||||
|                     child: ActivityTextField( | ||||
|                       isEnabled: album.activityEnabled, | ||||
|                       likeId: liked?.id, | ||||
|                       onSubmit: onAddComment, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|   | ||||
							
								
								
									
										105
									
								
								mobile/lib/modules/activities/widgets/activity_text_field.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								mobile/lib/modules/activities/widgets/activity_text_field.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/current_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/user.provider.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
|  | ||||
| class ActivityTextField extends HookConsumerWidget { | ||||
|   final bool isEnabled; | ||||
|   final String? likeId; | ||||
|   final Function(String) onSubmit; | ||||
|  | ||||
|   const ActivityTextField({ | ||||
|     required this.onSubmit, | ||||
|     this.isEnabled = true, | ||||
|     this.likeId, | ||||
|     super.key, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final album = ref.watch(currentAlbumProvider)!; | ||||
|     final asset = ref.watch(currentAssetProvider); | ||||
|     final activityNotifier = ref | ||||
|         .read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier); | ||||
|     final user = ref.watch(currentUserProvider); | ||||
|     final inputController = useTextEditingController(); | ||||
|     final inputFocusNode = useFocusNode(); | ||||
|     final liked = likeId != null; | ||||
|  | ||||
|     // Show keyboard immediately on activities open | ||||
|     useEffect( | ||||
|       () { | ||||
|         inputFocusNode.requestFocus(); | ||||
|         return null; | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     // Pass text to callback and reset controller | ||||
|     void onEditingComplete() { | ||||
|       onSubmit(inputController.text); | ||||
|       inputController.clear(); | ||||
|       inputFocusNode.unfocus(); | ||||
|     } | ||||
|  | ||||
|     Future<void> addLike() async { | ||||
|       await activityNotifier.addLike(); | ||||
|     } | ||||
|  | ||||
|     Future<void> removeLike() async { | ||||
|       if (liked) { | ||||
|         await activityNotifier.removeActivity(likeId!); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return Padding( | ||||
|       padding: const EdgeInsets.only(bottom: 10), | ||||
|       child: TextField( | ||||
|         controller: inputController, | ||||
|         enabled: isEnabled, | ||||
|         focusNode: inputFocusNode, | ||||
|         textInputAction: TextInputAction.send, | ||||
|         autofocus: false, | ||||
|         decoration: InputDecoration( | ||||
|           border: InputBorder.none, | ||||
|           focusedBorder: InputBorder.none, | ||||
|           prefixIcon: user != null | ||||
|               ? Padding( | ||||
|                   padding: const EdgeInsets.symmetric(horizontal: 15), | ||||
|                   child: UserCircleAvatar( | ||||
|                     user: user, | ||||
|                     size: 30, | ||||
|                     radius: 15, | ||||
|                   ), | ||||
|                 ) | ||||
|               : null, | ||||
|           suffixIcon: Padding( | ||||
|             padding: const EdgeInsets.only(right: 10), | ||||
|             child: IconButton( | ||||
|               icon: Icon( | ||||
|                 liked ? Icons.favorite_rounded : Icons.favorite_border_rounded, | ||||
|               ), | ||||
|               onPressed: liked ? removeLike : addLike, | ||||
|             ), | ||||
|           ), | ||||
|           suffixIconColor: liked ? Colors.red[700] : null, | ||||
|           hintText: !isEnabled | ||||
|               ? 'shared_album_activities_input_disable'.tr() | ||||
|               : 'shared_album_activities_input_hint'.tr(), | ||||
|           hintStyle: TextStyle( | ||||
|             fontWeight: FontWeight.normal, | ||||
|             fontSize: 14, | ||||
|             color: Colors.grey[600], | ||||
|           ), | ||||
|         ), | ||||
|         onEditingComplete: onEditingComplete, | ||||
|         onTapOutside: (_) => inputFocusNode.unfocus(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										116
									
								
								mobile/lib/modules/activities/widgets/activity_tile.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								mobile/lib/modules/activities/widgets/activity_tile.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/extensions/datetime_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/activities/models/activity.model.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_image.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
|  | ||||
| class ActivityTile extends HookConsumerWidget { | ||||
|   final Activity activity; | ||||
|  | ||||
|   const ActivityTile(this.activity, {super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final asset = ref.watch(currentAssetProvider); | ||||
|     final isLike = activity.type == ActivityType.like; | ||||
|     // Asset thumbnail is displayed when we are accessing activities from the album page | ||||
|     // currentAssetProvider will not be set until we open the gallery viewer | ||||
|     final showAssetThumbnail = asset == null && activity.assetId != null; | ||||
|  | ||||
|     return ListTile( | ||||
|       minVerticalPadding: 15, | ||||
|       leading: isLike | ||||
|           ? Container( | ||||
|               width: 44, | ||||
|               alignment: Alignment.center, | ||||
|               child: Icon( | ||||
|                 Icons.favorite_rounded, | ||||
|                 color: Colors.red[700], | ||||
|               ), | ||||
|             ) | ||||
|           : UserCircleAvatar(user: activity.user), | ||||
|       title: _ActivityTitle( | ||||
|         userName: activity.user.name, | ||||
|         createdAt: activity.createdAt.timeAgo(), | ||||
|         leftAlign: isLike || showAssetThumbnail, | ||||
|       ), | ||||
|       // No subtitle for like, so center title | ||||
|       titleAlignment: | ||||
|           !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center, | ||||
|       trailing: showAssetThumbnail | ||||
|           ? _ActivityAssetThumbnail(activity.assetId!) | ||||
|           : null, | ||||
|       subtitle: !isLike ? Text(activity.comment!) : null, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _ActivityTitle extends StatelessWidget { | ||||
|   final String userName; | ||||
|   final String createdAt; | ||||
|   final bool leftAlign; | ||||
|  | ||||
|   const _ActivityTitle({ | ||||
|     required this.userName, | ||||
|     required this.createdAt, | ||||
|     required this.leftAlign, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final textColor = context.isDarkTheme ? Colors.white : Colors.black; | ||||
|     final textStyle = context.textTheme.bodyMedium | ||||
|         ?.copyWith(color: textColor.withOpacity(0.6)); | ||||
|  | ||||
|     return Row( | ||||
|       mainAxisAlignment: | ||||
|           leftAlign ? MainAxisAlignment.start : MainAxisAlignment.spaceBetween, | ||||
|       mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max, | ||||
|       children: [ | ||||
|         Text( | ||||
|           userName, | ||||
|           style: textStyle, | ||||
|           overflow: TextOverflow.ellipsis, | ||||
|         ), | ||||
|         if (leftAlign) | ||||
|           Text( | ||||
|             " • ", | ||||
|             style: textStyle, | ||||
|           ), | ||||
|         Expanded( | ||||
|           child: Text( | ||||
|             createdAt, | ||||
|             style: textStyle, | ||||
|             overflow: TextOverflow.ellipsis, | ||||
|             textAlign: leftAlign ? TextAlign.left : TextAlign.right, | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _ActivityAssetThumbnail extends StatelessWidget { | ||||
|   final String assetId; | ||||
|  | ||||
|   const _ActivityAssetThumbnail(this.assetId); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       width: 40, | ||||
|       height: 30, | ||||
|       decoration: BoxDecoration( | ||||
|         borderRadius: const BorderRadius.all(Radius.circular(4)), | ||||
|         image: DecorationImage( | ||||
|           image: ImmichImage.remoteThumbnailProviderForId(assetId), | ||||
|           fit: BoxFit.cover, | ||||
|         ), | ||||
|       ), | ||||
|       child: const SizedBox.shrink(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,75 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart'; | ||||
| import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; | ||||
|  | ||||
| /// Wraps an [ActivityTile] and makes it dismissible | ||||
| class DismissibleActivity extends StatelessWidget { | ||||
|   final String activityId; | ||||
|   final ActivityTile body; | ||||
|   final Function(String)? onDismiss; | ||||
|  | ||||
|   const DismissibleActivity( | ||||
|     this.activityId, | ||||
|     this.body, { | ||||
|     this.onDismiss, | ||||
|     super.key, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Dismissible( | ||||
|       key: Key(activityId), | ||||
|       dismissThresholds: const { | ||||
|         DismissDirection.horizontal: 0.7, | ||||
|       }, | ||||
|       direction: DismissDirection.horizontal, | ||||
|       confirmDismiss: (direction) => onDismiss != null | ||||
|           ? showDialog( | ||||
|               context: context, | ||||
|               builder: (context) => ConfirmDialog( | ||||
|                 onOk: () {}, | ||||
|                 title: "shared_album_activity_remove_title", | ||||
|                 content: "shared_album_activity_remove_content", | ||||
|                 ok: "delete_dialog_ok", | ||||
|               ), | ||||
|             ) | ||||
|           : Future.value(false), | ||||
|       onDismissed: (_) async => onDismiss?.call(activityId), | ||||
|       // LTR | ||||
|       background: _DismissBackground(withDeleteIcon: onDismiss != null), | ||||
|       // RTL | ||||
|       secondaryBackground: _DismissBackground( | ||||
|         withDeleteIcon: onDismiss != null, | ||||
|         alignment: AlignmentDirectional.centerEnd, | ||||
|       ), | ||||
|       child: body, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _DismissBackground extends StatelessWidget { | ||||
|   final AlignmentDirectional alignment; | ||||
|   final bool withDeleteIcon; | ||||
|  | ||||
|   const _DismissBackground({ | ||||
|     required this.withDeleteIcon, | ||||
|     this.alignment = AlignmentDirectional.centerStart, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       alignment: alignment, | ||||
|       color: withDeleteIcon ? Colors.red[400] : Colors.grey[600], | ||||
|       child: withDeleteIcon | ||||
|           ? const Padding( | ||||
|               padding: EdgeInsets.all(15), | ||||
|               child: Icon( | ||||
|                 Icons.delete_sweep_rounded, | ||||
|                 color: Colors.black, | ||||
|               ), | ||||
|             ) | ||||
|           : null, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,7 +7,7 @@ part of 'album_sort_by_options.provider.dart'; | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| String _$albumSortByOptionsHash() => | ||||
|     r'8d22fa8b7cbca2d3d7ed20a83bf00211dc948004'; | ||||
|     r'dd8da5e730af555de1b86c3b157b6c93183523ac'; | ||||
| 
 | ||||
| /// See also [AlbumSortByOptions]. | ||||
| @ProviderFor(AlbumSortByOptions) | ||||
|   | ||||
| @@ -1,6 +1,15 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| final currentAlbumProvider = StateProvider<Album?>((ref) { | ||||
|   return null; | ||||
| }); | ||||
| part 'current_album.provider.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| class CurrentAlbum extends _$CurrentAlbum { | ||||
|   @override | ||||
|   Album? build() => null; | ||||
|  | ||||
|   void set(Album? a) => state = a; | ||||
| } | ||||
|  | ||||
| /// Mock class for testing | ||||
| abstract class CurrentAlbumInternal extends _$CurrentAlbum {} | ||||
|   | ||||
							
								
								
									
										25
									
								
								mobile/lib/modules/album/providers/current_album.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								mobile/lib/modules/album/providers/current_album.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'current_album.provider.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| String _$currentAlbumHash() => r'61f00273d6b69da45add1532cc3d3a076ee55110'; | ||||
| 
 | ||||
| /// See also [CurrentAlbum]. | ||||
| @ProviderFor(CurrentAlbum) | ||||
| final currentAlbumProvider = | ||||
|     AutoDisposeNotifierProvider<CurrentAlbum, Album?>.internal( | ||||
|   CurrentAlbum.new, | ||||
|   name: r'currentAlbumProvider', | ||||
|   debugGetCreateSourceHash: | ||||
|       const bool.fromEnvironment('dart.vm.product') ? null : _$currentAlbumHash, | ||||
|   dependencies: null, | ||||
|   allTransitiveDependencies: null, | ||||
| ); | ||||
| 
 | ||||
| typedef _$CurrentAlbum = AutoDisposeNotifier<Album?>; | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -104,7 +105,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget { | ||||
|                           style: TextStyle(color: context.primaryColor), | ||||
|                         ), | ||||
|                         onPressed: () { | ||||
|                           context.autoPush( | ||||
|                           context.pushRoute( | ||||
|                             CreateAlbumRoute( | ||||
|                               isSharedAlbum: false, | ||||
|                               initialAssets: assets, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -60,7 +61,7 @@ class AlbumThumbnailListTile extends StatelessWidget { | ||||
|       behavior: HitTestBehavior.opaque, | ||||
|       onTap: onTap ?? | ||||
|           () { | ||||
|             context.autoPush(AlbumViewerRoute(albumId: album.id)); | ||||
|             context.pushRoute(AlbumViewerRoute(albumId: album.id)); | ||||
|           }, | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.only(bottom: 12.0), | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:fluttertoast/fluttertoast.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; | ||||
| @@ -37,11 +38,7 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|     final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum; | ||||
|     final isProcessing = useProcessingOverlay(); | ||||
|     final comments = album.shared | ||||
|         ? ref.watch( | ||||
|             activityStatisticsStateProvider( | ||||
|               (albumId: album.remoteId!, assetId: null), | ||||
|             ), | ||||
|           ) | ||||
|         ? ref.watch(activityStatisticsProvider(album.remoteId!)) | ||||
|         : 0; | ||||
|  | ||||
|     deleteAlbum() async { | ||||
| @@ -52,11 +49,11 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|         success = | ||||
|             await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album); | ||||
|         context | ||||
|             .autoNavigate(const TabControllerRoute(children: [SharingRoute()])); | ||||
|             .navigateTo(const TabControllerRoute(children: [SharingRoute()])); | ||||
|       } else { | ||||
|         success = await ref.watch(albumProvider.notifier).deleteAlbum(album); | ||||
|         context | ||||
|             .autoNavigate(const TabControllerRoute(children: [LibraryRoute()])); | ||||
|             .navigateTo(const TabControllerRoute(children: [LibraryRoute()])); | ||||
|       } | ||||
|       if (!success) { | ||||
|         ImmichToast.show( | ||||
| @@ -122,7 +119,7 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|  | ||||
|       if (isSuccess) { | ||||
|         context | ||||
|             .autoNavigate(const TabControllerRoute(children: [SharingRoute()])); | ||||
|             .navigateTo(const TabControllerRoute(children: [SharingRoute()])); | ||||
|       } else { | ||||
|         context.pop(); | ||||
|         ImmichToast.show( | ||||
| @@ -175,7 +172,7 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|         ListTile( | ||||
|           leading: const Icon(Icons.share_rounded), | ||||
|           onTap: () { | ||||
|             context.autoPush(SharedLinkEditRoute(albumId: album.remoteId)); | ||||
|             context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId)); | ||||
|             context.pop(); | ||||
|           }, | ||||
|           title: const Text( | ||||
| @@ -185,7 +182,7 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|         ), | ||||
|         ListTile( | ||||
|           leading: const Icon(Icons.settings_rounded), | ||||
|           onTap: () => context.autoNavigate(AlbumOptionsRoute(album: album)), | ||||
|           onTap: () => context.navigateTo(AlbumOptionsRoute(album: album)), | ||||
|           title: const Text( | ||||
|             "translated_text_options", | ||||
|             style: TextStyle(fontWeight: FontWeight.w500), | ||||
| @@ -280,7 +277,7 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|         ); | ||||
|       } else { | ||||
|         return IconButton( | ||||
|           onPressed: () async => await context.autoPop(), | ||||
|           onPressed: () async => await context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|           splashRadius: 25, | ||||
|         ); | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -45,7 +46,7 @@ class AlbumOptionsPage extends HookConsumerWidget { | ||||
|             await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album); | ||||
|  | ||||
|         if (isSuccess) { | ||||
|           context.autoNavigate( | ||||
|           context.navigateTo( | ||||
|             const TabControllerRoute(children: [SharingRoute()]), | ||||
|           ); | ||||
|         } else { | ||||
| @@ -181,7 +182,7 @@ class AlbumOptionsPage extends HookConsumerWidget { | ||||
|       appBar: AppBar( | ||||
|         leading: IconButton( | ||||
|           icon: const Icon(Icons.arrow_back_ios_new_rounded), | ||||
|           onPressed: () => context.autoPop(null), | ||||
|           onPressed: () => context.popRoute(null), | ||||
|         ), | ||||
|         centerTitle: true, | ||||
|         title: Text("translated_text_options".tr()), | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -33,9 +36,12 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     FocusNode titleFocusNode = useFocusNode(); | ||||
|     final album = ref.watch(albumWatcher(albumId)); | ||||
|     // Listen provider to prevent autoDispose when navigating to other routes from within the viewer page | ||||
|     ref.listen(currentAlbumProvider, (_, __) {}); | ||||
|     album.whenData( | ||||
|       (value) => | ||||
|           Future((() => ref.read(currentAlbumProvider.notifier).state = value)), | ||||
|       (value) => Future.microtask( | ||||
|         () => ref.read(currentAlbumProvider.notifier).set(value), | ||||
|       ), | ||||
|     ); | ||||
|     final userId = ref.watch(authenticationProvider).userId; | ||||
|     final isProcessing = useProcessingOverlay(); | ||||
| @@ -62,7 +68,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|     /// If they exist, add to selected asset state to show they are already selected. | ||||
|     void onAddPhotosPressed(Album albumInfo) async { | ||||
|       AssetSelectionPageResult? returnPayload = | ||||
|           await context.autoPush<AssetSelectionPageResult?>( | ||||
|           await context.pushRoute<AssetSelectionPageResult?>( | ||||
|         AssetSelectionRoute( | ||||
|           existingAssets: albumInfo.assets, | ||||
|           canDeselect: false, | ||||
| @@ -84,7 +90,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     void onAddUsersPressed(Album album) async { | ||||
|       List<String>? sharedUserIds = await context.autoPush<List<String>?>( | ||||
|       List<String>? sharedUserIds = await context.pushRoute<List<String>?>( | ||||
|         SelectAdditionalUserForSharingRoute(album: album), | ||||
|       ); | ||||
|  | ||||
| @@ -178,7 +184,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|     Widget buildSharedUserIconsRow(Album album) { | ||||
|       return GestureDetector( | ||||
|         onTap: () => context.autoPush(AlbumOptionsRoute(album: album)), | ||||
|         onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)), | ||||
|         child: SizedBox( | ||||
|           height: 50, | ||||
|           child: ListView.builder( | ||||
| @@ -214,13 +220,8 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|     onActivitiesPressed(Album album) { | ||||
|       if (album.remoteId != null) { | ||||
|         context.autoPush( | ||||
|           ActivitiesRoute( | ||||
|             albumId: album.remoteId!, | ||||
|             appBarTitle: album.name, | ||||
|             isOwner: userId == album.ownerId, | ||||
|             isReadOnly: !album.activityEnabled, | ||||
|           ), | ||||
|         context.pushRoute( | ||||
|           const ActivitiesRoute(), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -36,7 +37,7 @@ class CreateAlbumPage extends HookConsumerWidget { | ||||
|     ); | ||||
|  | ||||
|     showSelectUserPage() async { | ||||
|       final bool? ok = await context.autoPush<bool?>( | ||||
|       final bool? ok = await context.pushRoute<bool?>( | ||||
|         SelectUserForSharingRoute(assets: selectedAssets.value), | ||||
|       ); | ||||
|       if (ok == true) { | ||||
| @@ -58,7 +59,7 @@ class CreateAlbumPage extends HookConsumerWidget { | ||||
|  | ||||
|     onSelectPhotosButtonPressed() async { | ||||
|       AssetSelectionPageResult? selectedAsset = | ||||
|           await context.autoPush<AssetSelectionPageResult?>( | ||||
|           await context.pushRoute<AssetSelectionPageResult?>( | ||||
|         AssetSelectionRoute( | ||||
|           existingAssets: selectedAssets.value, | ||||
|           canDeselect: true, | ||||
| @@ -202,7 +203,7 @@ class CreateAlbumPage extends HookConsumerWidget { | ||||
|         selectedAssets.value = {}; | ||||
|         ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); | ||||
|  | ||||
|         context.autoReplace(AlbumViewerRoute(albumId: newAlbum.id)); | ||||
|         context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -214,7 +215,7 @@ class CreateAlbumPage extends HookConsumerWidget { | ||||
|         leading: IconButton( | ||||
|           onPressed: () { | ||||
|             selectedAssets.value = {}; | ||||
|             context.autoPop(); | ||||
|             context.popRoute(); | ||||
|           }, | ||||
|           icon: const Icon(Icons.close_rounded), | ||||
|         ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -102,7 +103,7 @@ class LibraryPage extends HookConsumerWidget { | ||||
|  | ||||
|           return GestureDetector( | ||||
|             onTap: () => | ||||
|                 context.autoPush(CreateAlbumRoute(isSharedAlbum: false)), | ||||
|                 context.pushRoute(CreateAlbumRoute(isSharedAlbum: false)), | ||||
|             child: Padding( | ||||
|               padding: | ||||
|                   const EdgeInsets.only(bottom: 32), // Adjust padding to suit | ||||
| @@ -190,7 +191,7 @@ class LibraryPage extends HookConsumerWidget { | ||||
|     Widget? shareTrashButton() { | ||||
|       return trashEnabled | ||||
|           ? InkWell( | ||||
|               onTap: () => context.autoPush(const TrashRoute()), | ||||
|               onTap: () => context.pushRoute(const TrashRoute()), | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(12)), | ||||
|               child: const Icon( | ||||
|                 Icons.delete_rounded, | ||||
| @@ -219,12 +220,12 @@ class LibraryPage extends HookConsumerWidget { | ||||
|                 children: [ | ||||
|                   buildLibraryNavButton( | ||||
|                       "library_page_favorites".tr(), Icons.favorite_border, () { | ||||
|                     context.autoNavigate(const FavoritesRoute()); | ||||
|                     context.navigateTo(const FavoritesRoute()); | ||||
|                   }), | ||||
|                   const SizedBox(width: 12.0), | ||||
|                   buildLibraryNavButton( | ||||
|                       "library_page_archive".tr(), Icons.archive_outlined, () { | ||||
|                     context.autoNavigate(const ArchiveRoute()); | ||||
|                     context.navigateTo(const ArchiveRoute()); | ||||
|                   }), | ||||
|                 ], | ||||
|               ), | ||||
| @@ -270,7 +271,7 @@ class LibraryPage extends HookConsumerWidget { | ||||
|  | ||||
|                   return AlbumThumbnailCard( | ||||
|                     album: sorted[index - 1], | ||||
|                     onTap: () => context.autoPush( | ||||
|                     onTap: () => context.pushRoute( | ||||
|                       AlbumViewerRoute( | ||||
|                         albumId: sorted[index - 1].id, | ||||
|                       ), | ||||
| @@ -314,7 +315,7 @@ class LibraryPage extends HookConsumerWidget { | ||||
|                 childCount: local.length, | ||||
|                 (context, index) => AlbumThumbnailCard( | ||||
|                   album: local[index], | ||||
|                   onTap: () => context.autoPush( | ||||
|                   onTap: () => context.pushRoute( | ||||
|                     AlbumViewerRoute( | ||||
|                       albumId: local[index].id, | ||||
|                     ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -22,7 +23,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|     final sharedUsersList = useState<Set<User>>({}); | ||||
|  | ||||
|     addNewUsersHandler() { | ||||
|       context.autoPop(sharedUsersList.value.map((e) => e.id).toList()); | ||||
|       context.popRoute(sharedUsersList.value.map((e) => e.id).toList()); | ||||
|     } | ||||
|  | ||||
|     buildTileIcon(User user) { | ||||
| @@ -123,7 +124,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { | ||||
|         leading: IconButton( | ||||
|           icon: const Icon(Icons.close_rounded), | ||||
|           onPressed: () { | ||||
|             context.autoPop(null); | ||||
|             context.popRoute(null); | ||||
|           }, | ||||
|         ), | ||||
|         actions: [ | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -35,9 +36,9 @@ class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|         await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); | ||||
|         // ref.watch(assetSelectionProvider.notifier).removeAll(); | ||||
|         ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); | ||||
|         context.autoPop(true); | ||||
|         context.popRoute(true); | ||||
|         context | ||||
|             .autoNavigate(const TabControllerRoute(children: [SharingRoute()])); | ||||
|             .navigateTo(const TabControllerRoute(children: [SharingRoute()])); | ||||
|       } | ||||
|  | ||||
|       ScaffoldMessenger( | ||||
| @@ -151,7 +152,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { | ||||
|         leading: IconButton( | ||||
|           icon: const Icon(Icons.close_rounded), | ||||
|           onPressed: () async { | ||||
|             context.autoPop(); | ||||
|             context.popRoute(); | ||||
|           }, | ||||
|         ), | ||||
|         actions: [ | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -48,11 +49,9 @@ class SharingPage extends HookConsumerWidget { | ||||
|               return AlbumThumbnailCard( | ||||
|                 album: sharedAlbums[index], | ||||
|                 showOwner: true, | ||||
|                 onTap: () { | ||||
|                   context.autoPush( | ||||
|                     AlbumViewerRoute(albumId: sharedAlbums[index].id), | ||||
|                   ); | ||||
|                 }, | ||||
|                 onTap: () => context.pushRoute( | ||||
|                   AlbumViewerRoute(albumId: sharedAlbums[index].id), | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|             childCount: sharedAlbums.length, | ||||
| @@ -99,11 +98,8 @@ class SharingPage extends HookConsumerWidget { | ||||
|                           style: context.textTheme.bodyMedium, | ||||
|                         ) | ||||
|                       : null, | ||||
|               onTap: () { | ||||
|                 context.autoPush( | ||||
|                   AlbumViewerRoute(albumId: sharedAlbums[index].id), | ||||
|                 ); | ||||
|               }, | ||||
|               onTap: () => context | ||||
|                   .pushRoute(AlbumViewerRoute(albumId: sharedAlbums[index].id)), | ||||
|             ); | ||||
|           }, | ||||
|           childCount: sharedAlbums.length, | ||||
| @@ -124,9 +120,8 @@ class SharingPage extends HookConsumerWidget { | ||||
|           children: [ | ||||
|             Expanded( | ||||
|               child: ElevatedButton.icon( | ||||
|                 onPressed: () { | ||||
|                   context.autoPush(CreateAlbumRoute(isSharedAlbum: true)); | ||||
|                 }, | ||||
|                 onPressed: () => | ||||
|                     context.pushRoute(CreateAlbumRoute(isSharedAlbum: true)), | ||||
|                 icon: const Icon( | ||||
|                   Icons.photo_album_outlined, | ||||
|                   size: 20, | ||||
| @@ -144,7 +139,7 @@ class SharingPage extends HookConsumerWidget { | ||||
|             const SizedBox(width: 12.0), | ||||
|             Expanded( | ||||
|               child: ElevatedButton.icon( | ||||
|                 onPressed: () => context.autoPush(const SharedLinkRoute()), | ||||
|                 onPressed: () => context.pushRoute(const SharedLinkRoute()), | ||||
|                 icon: const Icon( | ||||
|                   Icons.link, | ||||
|                   size: 20, | ||||
| @@ -214,7 +209,7 @@ class SharingPage extends HookConsumerWidget { | ||||
|  | ||||
|     Widget sharePartnerButton() { | ||||
|       return InkWell( | ||||
|         onTap: () => context.autoPush(const PartnerRoute()), | ||||
|         onTap: () => context.pushRoute(const PartnerRoute()), | ||||
|         borderRadius: const BorderRadius.all(Radius.circular(12)), | ||||
|         child: const Icon( | ||||
|           Icons.swap_horizontal_circle_rounded, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; | ||||
| import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; | ||||
| @@ -16,7 +16,7 @@ class ArchivePage extends HookConsumerWidget { | ||||
|       final count = archivedAssets.value?.totalAssets.toString() ?? "?"; | ||||
|       return AppBar( | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|         ), | ||||
|         centerTitle: true, | ||||
|   | ||||
| @@ -0,0 +1,15 @@ | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| part 'current_asset.provider.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| class CurrentAsset extends _$CurrentAsset { | ||||
|   @override | ||||
|   Asset? build() => null; | ||||
|  | ||||
|   void set(Asset? a) => state = a; | ||||
| } | ||||
|  | ||||
| /// Mock class for testing | ||||
| abstract class CurrentAssetInternal extends _$CurrentAsset {} | ||||
							
								
								
									
										25
									
								
								mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'current_asset.provider.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| String _$currentAssetHash() => r'018d9f936991c48f06c11bf7e72130bba25806e2'; | ||||
| 
 | ||||
| /// See also [CurrentAsset]. | ||||
| @ProviderFor(CurrentAsset) | ||||
| final currentAssetProvider = | ||||
|     AutoDisposeNotifierProvider<CurrentAsset, Asset?>.internal( | ||||
|   CurrentAsset.new, | ||||
|   name: r'currentAssetProvider', | ||||
|   debugGetCreateSourceHash: | ||||
|       const bool.fromEnvironment('dart.vm.product') ? null : _$currentAssetHash, | ||||
|   dependencies: null, | ||||
|   allTransitiveDependencies: null, | ||||
| ); | ||||
| 
 | ||||
| typedef _$CurrentAsset = AutoDisposeNotifier<Asset?>; | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/current_album.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; | ||||
| @@ -39,12 +39,8 @@ class TopControlAppBar extends HookConsumerWidget { | ||||
|     const double iconSize = 22.0; | ||||
|     final a = ref.watch(assetWatcher(asset)).value ?? asset; | ||||
|     final album = ref.watch(currentAlbumProvider); | ||||
|     final comments = album != null && album.remoteId != null | ||||
|         ? ref.watch( | ||||
|             activityStatisticsStateProvider( | ||||
|               (albumId: album.remoteId!, assetId: asset.remoteId), | ||||
|             ), | ||||
|           ) | ||||
|     final comments = album != null | ||||
|         ? ref.watch(activityStatisticsProvider(album.remoteId!, asset.remoteId)) | ||||
|         : 0; | ||||
|  | ||||
|     Widget buildFavoriteButton(a) { | ||||
| @@ -149,7 +145,7 @@ class TopControlAppBar extends HookConsumerWidget { | ||||
|     Widget buildBackButton() { | ||||
|       return IconButton( | ||||
|         onPressed: () { | ||||
|           context.autoPop(); | ||||
|           context.popRoute(); | ||||
|         }, | ||||
|         icon: Icon( | ||||
|           Icons.arrow_back_ios_new_rounded, | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/current_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart'; | ||||
| @@ -106,6 +107,19 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|     bool isParent = stackIndex.value == -1 || stackIndex.value == 0; | ||||
|  | ||||
|     // Listen provider to prevent autoDispose when navigating to other routes from within the gallery page | ||||
|     ref.listen(currentAssetProvider, (_, __) {}); | ||||
|     useEffect( | ||||
|       () { | ||||
|         // Delay state update to after the execution of build method | ||||
|         Future.microtask( | ||||
|           () => ref.read(currentAssetProvider.notifier).set(asset()), | ||||
|         ); | ||||
|         return null; | ||||
|       }, | ||||
|       [asset()], | ||||
|     ); | ||||
|  | ||||
|     useEffect( | ||||
|       () { | ||||
|         isLoadPreview.value = | ||||
| @@ -214,7 +228,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|         if (isDeleted && isParent) { | ||||
|           if (totalAssets == 1) { | ||||
|             // Handle only one asset | ||||
|             context.autoPop(); | ||||
|             context.popRoute(); | ||||
|           } else { | ||||
|             // Go to next page otherwise | ||||
|             controller.nextPage( | ||||
| @@ -298,7 +312,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|       final ratio = d.dy / max(d.dx.abs(), 1); | ||||
|       if (d.dy > sensitivity && ratio > ratioThreshold) { | ||||
|         context.autoPop(); | ||||
|         context.popRoute(); | ||||
|       } else if (d.dy < -sensitivity && ratio < -ratioThreshold) { | ||||
|         showInfo(); | ||||
|       } | ||||
| @@ -311,7 +325,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|     handleArchive(Asset asset) { | ||||
|       ref.watch(assetProvider.notifier).toggleArchive([asset]); | ||||
|       if (isParent) { | ||||
|         context.autoPop(); | ||||
|         context.popRoute(); | ||||
|         return; | ||||
|       } | ||||
|       removeAssetFromStack(); | ||||
| @@ -334,14 +348,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|     handleActivities() { | ||||
|       if (album != null && album.shared && album.remoteId != null) { | ||||
|         context.autoPush( | ||||
|           ActivitiesRoute( | ||||
|             albumId: album.remoteId!, | ||||
|             assetId: asset().remoteId, | ||||
|             withAssetThumbs: false, | ||||
|             isOwner: isOwner, | ||||
|           ), | ||||
|         ); | ||||
|         context.pushRoute(const ActivitiesRoute()); | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -517,7 +524,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|                               stackElements.elementAt(stackIndex.value), | ||||
|                             ); | ||||
|                         ctx.pop(); | ||||
|                         context.autoPop(); | ||||
|                         context.popRoute(); | ||||
|                       }, | ||||
|                       title: const Text( | ||||
|                         "viewer_stack_use_as_main_asset", | ||||
| @@ -544,7 +551,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|                           childrenToRemove: [currentAsset], | ||||
|                         ); | ||||
|                         ctx.pop(); | ||||
|                         context.autoPop(); | ||||
|                         context.popRoute(); | ||||
|                       } else { | ||||
|                         await ref.read(assetStackServiceProvider).updateStack( | ||||
|                           currentAsset, | ||||
| @@ -572,7 +579,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|                             childrenToRemove: stack, | ||||
|                           ); | ||||
|                       ctx.pop(); | ||||
|                       context.autoPop(); | ||||
|                       context.popRoute(); | ||||
|                     }, | ||||
|                     title: const Text( | ||||
|                       "viewer_unstack", | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| @@ -201,7 +202,7 @@ class AlbumInfoCard extends HookConsumerWidget { | ||||
|                   ), | ||||
|                   IconButton( | ||||
|                     onPressed: () { | ||||
|                       context.autoPush( | ||||
|                       context.pushRoute( | ||||
|                         AlbumPreviewRoute(album: albumInfo.albumEntity), | ||||
|                       ); | ||||
|                     }, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -134,7 +135,7 @@ class AlbumInfoListTile extends HookConsumerWidget { | ||||
|         subtitle: Text(assetCount.value.toString()), | ||||
|         trailing: IconButton( | ||||
|           onPressed: () { | ||||
|             context.autoPush( | ||||
|             context.pushRoute( | ||||
|               AlbumPreviewRoute(album: albumInfo.albumEntity), | ||||
|             ); | ||||
|           }, | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -56,9 +57,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { | ||||
|           args: [ref.watch(errorBackupListProvider).length.toString()], | ||||
|         ), | ||||
|         backgroundColor: Colors.white, | ||||
|         onPressed: () { | ||||
|           context.autoPush(const FailedBackupStatusRoute()); | ||||
|         }, | ||||
|         onPressed: () => context.pushRoute(const FailedBackupStatusRoute()), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import 'dart:typed_data'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
|  | ||||
| @@ -53,7 +53,7 @@ class AlbumPreviewPage extends HookConsumerWidget { | ||||
|           ], | ||||
|         ), | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_new_rounded), | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -193,7 +194,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|         ), | ||||
|         title: const Text( | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import 'dart:io'; | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -151,7 +152,7 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|             ), | ||||
|             trailing: ElevatedButton( | ||||
|               onPressed: () async { | ||||
|                 await context.autoPush(const BackupAlbumSelectionRoute()); | ||||
|                 await context.pushRoute(const BackupAlbumSelectionRoute()); | ||||
|                 // waited until returning from selection | ||||
|                 await ref | ||||
|                     .read(backupProvider.notifier) | ||||
| @@ -242,7 +243,7 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|         leading: IconButton( | ||||
|           onPressed: () { | ||||
|             ref.watch(websocketProvider.notifier).listenUploadEvent(); | ||||
|             context.autoPop(true); | ||||
|             context.popRoute(true); | ||||
|           }, | ||||
|           splashRadius: 24, | ||||
|           icon: const Icon( | ||||
| @@ -253,7 +254,7 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.only(right: 8.0), | ||||
|             child: IconButton( | ||||
|               onPressed: () => context.autoPush(const BackupOptionsRoute()), | ||||
|               onPressed: () => context.pushRoute(const BackupOptionsRoute()), | ||||
|               splashRadius: 24, | ||||
|               icon: const Icon( | ||||
|                 Icons.settings_outlined, | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:connectivity_plus/connectivity_plus.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -487,9 +488,7 @@ class BackupOptionsPage extends HookConsumerWidget { | ||||
|           "Backup options", | ||||
|         ), | ||||
|         leading: IconButton( | ||||
|           onPressed: () { | ||||
|             context.autoPop(true); | ||||
|           }, | ||||
|           onPressed: () => context.popRoute(true), | ||||
|           splashRadius: 24, | ||||
|           icon: const Icon( | ||||
|             Icons.arrow_back_ios_rounded, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| @@ -20,7 +21,7 @@ class FailedBackupStatusPage extends HookConsumerWidget { | ||||
|         ), | ||||
|         leading: IconButton( | ||||
|           onPressed: () { | ||||
|             context.autoPop(true); | ||||
|             context.popRoute(true); | ||||
|           }, | ||||
|           splashRadius: 24, | ||||
|           icon: const Icon( | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; | ||||
| import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; | ||||
| @@ -14,7 +14,7 @@ class FavoritesPage extends HookConsumerWidget { | ||||
|     AppBar buildAppBar() { | ||||
|       return AppBar( | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|         ), | ||||
|         centerTitle: true, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| @@ -174,7 +175,7 @@ class ThumbnailImage extends StatelessWidget { | ||||
|             onSelect?.call(); | ||||
|           } | ||||
|         } else { | ||||
|           context.autoPush( | ||||
|           context.pushRoute( | ||||
|             GalleryViewerRoute( | ||||
|               initialIndex: index, | ||||
|               loadAsset: loadAsset, | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'dart:io'; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| @@ -157,7 +158,7 @@ class LoginForm extends HookConsumerWidget { | ||||
|           // Resume backup (if enable) then navigate | ||||
|           if (ref.read(authenticationProvider).shouldChangePassword && | ||||
|               !ref.read(authenticationProvider).isAdmin) { | ||||
|             context.autoPush(const ChangePasswordRoute()); | ||||
|             context.pushRoute(const ChangePasswordRoute()); | ||||
|           } else { | ||||
|             final hasPermission = await ref | ||||
|                 .read(galleryPermissionNotifier.notifier) | ||||
| @@ -166,7 +167,7 @@ class LoginForm extends HookConsumerWidget { | ||||
|               // Don't resume the backup until we have gallery permission | ||||
|               ref.read(backupProvider.notifier).resumeBackup(); | ||||
|             } | ||||
|             context.autoReplace(const TabControllerRoute()); | ||||
|             context.replaceRoute(const TabControllerRoute()); | ||||
|           } | ||||
|         } else { | ||||
|           ImmichToast.show( | ||||
| @@ -218,7 +219,7 @@ class LoginForm extends HookConsumerWidget { | ||||
|             if (permission.isGranted || permission.isLimited) { | ||||
|               ref.watch(backupProvider.notifier).resumeBackup(); | ||||
|             } | ||||
|             context.autoReplace(const TabControllerRoute()); | ||||
|             context.replaceRoute(const TabControllerRoute()); | ||||
|           } else { | ||||
|             ImmichToast.show( | ||||
|               context: context, | ||||
| @@ -264,7 +265,7 @@ class LoginForm extends HookConsumerWidget { | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   onPressed: () => context.autoPush(const SettingsRoute()), | ||||
|                   onPressed: () => context.pushRoute(const SettingsRoute()), | ||||
|                   icon: const Icon(Icons.settings_rounded), | ||||
|                   label: const SizedBox.shrink(), | ||||
|                 ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -53,7 +54,7 @@ class LoginPage extends HookConsumerWidget { | ||||
|                   ), | ||||
|                 ), | ||||
|                 onTap: () { | ||||
|                   context.autoPush(const AppLogRoute()); | ||||
|                   context.pushRoute(const AppLogRoute()); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -90,12 +91,12 @@ class MapLocationPickerPage extends HookConsumerWidget { | ||||
|                   mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||||
|                   children: [ | ||||
|                     ElevatedButton( | ||||
|                       onPressed: () => context.autoPop(selectedLatLng.value), | ||||
|                       onPressed: () => context.popRoute(selectedLatLng.value), | ||||
|                       child: const Text("map_location_picker_page_use_location") | ||||
|                           .tr(), | ||||
|                     ), | ||||
|                     ElevatedButton( | ||||
|                       onPressed: () => context.autoPop(), | ||||
|                       onPressed: () => context.popRoute(), | ||||
|                       style: ElevatedButton.styleFrom( | ||||
|                         backgroundColor: context.colorScheme.error, | ||||
|                       ), | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/asset_grid/disable_multi_select_button.dart'; | ||||
| import 'package:immich_mobile/modules/map/ui/map_settings_dialog.dart'; | ||||
|  | ||||
| @@ -30,7 +30,7 @@ class MapAppBar extends HookWidget implements PreferredSizeWidget { | ||||
|       Padding( | ||||
|         padding: const EdgeInsets.only(left: 15, top: 15), | ||||
|         child: ElevatedButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           style: ElevatedButton.styleFrom( | ||||
|             shape: const CircleBorder(), | ||||
|             padding: const EdgeInsets.all(12), | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -102,7 +103,7 @@ class MapPageState extends ConsumerState<MapPage> { | ||||
|   } | ||||
|  | ||||
|   void openAssetInViewer(Asset asset) { | ||||
|     context.autoPush( | ||||
|     context.pushRoute( | ||||
|       GalleryViewerRoute( | ||||
|         initialIndex: 0, | ||||
|         loadAsset: (index) => asset, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/memories/providers/memory.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_image.dart'; | ||||
| @@ -31,7 +31,7 @@ class MemoryLane extends HookConsumerWidget { | ||||
|                         child: GestureDetector( | ||||
|                           onTap: () { | ||||
|                             HapticFeedback.heavyImpact(); | ||||
|                             context.autoPush( | ||||
|                             context.pushRoute( | ||||
|                               MemoryRoute( | ||||
|                                 memories: memories, | ||||
|                                 memoryIndex: index, | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/memories/models/memory.dart'; | ||||
| import 'package:immich_mobile/modules/memories/ui/memory_card.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| @@ -182,14 +182,14 @@ class MemoryPage extends HookConsumerWidget { | ||||
|                   currentMemory.value.assets.length; | ||||
|               if (isLastAsset && | ||||
|                   (offset > notification.metrics.maxScrollExtent + 150)) { | ||||
|                 context.autoPop(); | ||||
|                 context.popRoute(); | ||||
|                 return true; | ||||
|               } | ||||
|             } | ||||
|             // Horizontal scroll handling | ||||
|             if (notification.depth == 1 && | ||||
|                 (offset > notification.metrics.maxScrollExtent + 100)) { | ||||
|               context.autoPop(); | ||||
|               context.popRoute(); | ||||
|               return true; | ||||
|             } | ||||
|           } | ||||
| @@ -244,7 +244,7 @@ class MemoryPage extends HookConsumerWidget { | ||||
|                           child: MemoryCard( | ||||
|                             asset: asset, | ||||
|                             onTap: () => toNextAsset(index), | ||||
|                             onClose: () => context.autoPop(), | ||||
|                             onClose: () => context.popRoute(), | ||||
|                             rightCornerText: assetProgress.value, | ||||
|                             title: memories[mIndex].title, | ||||
|                             showTitle: index == 0, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -16,7 +17,7 @@ class PermissionOnboardingPage extends HookConsumerWidget { | ||||
|     final PermissionStatus permission = ref.watch(galleryPermissionNotifier); | ||||
|  | ||||
|     // Navigate to the main Tab Controller when permission is granted | ||||
|     void goToBackup() => context.autoReplace(const BackupControllerRoute()); | ||||
|     void goToBackup() => context.replaceRoute(const BackupControllerRoute()); | ||||
|  | ||||
|     // When the permission is denied, we show a request permission page | ||||
|     buildRequestPermission() { | ||||
| @@ -174,7 +175,7 @@ class PermissionOnboardingPage extends HookConsumerWidget { | ||||
|                 ), | ||||
|                 TextButton( | ||||
|                   child: const Text('permission_onboarding_back').tr(), | ||||
|                   onPressed: () => context.autoPop(), | ||||
|                   onPressed: () => context.popRoute(), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| @@ -36,7 +37,7 @@ class PartnerList extends HookConsumerWidget { | ||||
|           color: context.primaryColor, | ||||
|         ), | ||||
|       ), | ||||
|       onTap: () => context.autoPush((PartnerDetailRoute(partner: p))), | ||||
|       onTap: () => context.pushRoute((PartnerDetailRoute(partner: p))), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| @@ -26,7 +27,7 @@ class CuratedPlacesRow extends CuratedRow { | ||||
|     final int actualContentIndex = isMapEnabled ? 1 : 0; | ||||
|     Widget buildMapThumbnail() { | ||||
|       return GestureDetector( | ||||
|         onTap: () => context.autoPush( | ||||
|         onTap: () => context.pushRoute( | ||||
|           const MapRoute(), | ||||
|         ), | ||||
|         child: SizedBox.square( | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/search/models/curated_content.dart'; | ||||
| import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| @@ -50,13 +50,13 @@ class ExploreGrid extends StatelessWidget { | ||||
|           borderRadius: 0, | ||||
|           onTap: () { | ||||
|             isPeople | ||||
|                 ? context.autoPush( | ||||
|                 ? context.pushRoute( | ||||
|                     PersonResultRoute( | ||||
|                       personId: content.id, | ||||
|                       personName: content.label, | ||||
|                     ), | ||||
|                   ) | ||||
|                 : context.autoPush( | ||||
|                 : context.pushRoute( | ||||
|                     SearchResultRoute(searchTerm: 'm:${content.label}'), | ||||
|                   ); | ||||
|           }, | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; | ||||
| import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart'; | ||||
|  | ||||
| @@ -17,7 +17,7 @@ class AllMotionPhotosPage extends HookConsumerWidget { | ||||
|       appBar: AppBar( | ||||
|         title: const Text('motion_photos_page_title').tr(), | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/search/providers/people.provider.dart'; | ||||
| import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; | ||||
|  | ||||
| @@ -19,7 +19,7 @@ class AllPeoplePage extends HookConsumerWidget { | ||||
|           'all_people_page_title', | ||||
|         ).tr(), | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; | ||||
| import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart'; | ||||
|  | ||||
| @@ -17,7 +17,7 @@ class AllVideosPage extends HookConsumerWidget { | ||||
|       appBar: AppBar( | ||||
|         title: const Text('all_videos_page_title').tr(), | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/search/models/curated_content.dart'; | ||||
| import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; | ||||
| import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; | ||||
| @@ -22,7 +22,7 @@ class CuratedLocationPage extends HookConsumerWidget { | ||||
|           'curated_location_page_title', | ||||
|         ).tr(), | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -101,7 +102,7 @@ class PersonResultPage extends HookConsumerWidget { | ||||
|       appBar: AppBar( | ||||
|         title: Text(name.value), | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|         ), | ||||
|         actions: [ | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; | ||||
| import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart'; | ||||
|  | ||||
| @@ -17,7 +17,7 @@ class RecentlyAddedPage extends HookConsumerWidget { | ||||
|       appBar: AppBar( | ||||
|         title: const Text('recently_added_page_title').tr(), | ||||
|         leading: IconButton( | ||||
|           onPressed: () => context.autoPop(), | ||||
|           onPressed: () => context.popRoute(), | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'dart:math' as math; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| @@ -52,7 +53,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|       searchFocusNode.unfocus(); | ||||
|       ref.watch(searchPageStateProvider.notifier).disableSearch(); | ||||
|  | ||||
|       context.autoPush( | ||||
|       context.pushRoute( | ||||
|         SearchResultRoute( | ||||
|           searchTerm: searchTerm, | ||||
|         ), | ||||
| @@ -79,7 +80,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|           onData: (people) => CuratedPeopleRow( | ||||
|             content: people.take(12).toList(), | ||||
|             onTap: (content, index) { | ||||
|               context.autoPush( | ||||
|               context.pushRoute( | ||||
|                 PersonResultRoute( | ||||
|                   personId: content.id, | ||||
|                   personName: content.label, | ||||
| @@ -111,7 +112,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                 .toList(), | ||||
|             imageSize: imageSize, | ||||
|             onTap: (content, index) { | ||||
|               context.autoPush( | ||||
|               context.pushRoute( | ||||
|                 SearchResultRoute( | ||||
|                   searchTerm: 'm:${content.label}', | ||||
|                 ), | ||||
| @@ -139,13 +140,13 @@ class SearchPage extends HookConsumerWidget { | ||||
|                 SearchRowTitle( | ||||
|                   title: "search_page_people".tr(), | ||||
|                   onViewAllPressed: () => | ||||
|                       context.autoPush(const AllPeopleRoute()), | ||||
|                       context.pushRoute(const AllPeopleRoute()), | ||||
|                 ), | ||||
|                 buildPeople(), | ||||
|                 SearchRowTitle( | ||||
|                   title: "search_page_places".tr(), | ||||
|                   onViewAllPressed: () => | ||||
|                       context.autoPush(const CuratedLocationRoute()), | ||||
|                       context.pushRoute(const CuratedLocationRoute()), | ||||
|                   top: 0, | ||||
|                 ), | ||||
|                 const SizedBox(height: 10.0), | ||||
| @@ -168,7 +169,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                   title: | ||||
|                       Text('search_page_favorites', style: categoryTitleStyle) | ||||
|                           .tr(), | ||||
|                   onTap: () => context.autoPush(const FavoritesRoute()), | ||||
|                   onTap: () => context.pushRoute(const FavoritesRoute()), | ||||
|                 ), | ||||
|                 const CategoryDivider(), | ||||
|                 ListTile( | ||||
| @@ -180,7 +181,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                     'search_page_recently_added', | ||||
|                     style: categoryTitleStyle, | ||||
|                   ).tr(), | ||||
|                   onTap: () => context.autoPush(const RecentlyAddedRoute()), | ||||
|                   onTap: () => context.pushRoute(const RecentlyAddedRoute()), | ||||
|                 ), | ||||
|                 const SizedBox(height: 24.0), | ||||
|                 Padding( | ||||
| @@ -200,7 +201,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                     Icons.screenshot, | ||||
|                     color: categoryIconColor, | ||||
|                   ), | ||||
|                   onTap: () => context.autoPush( | ||||
|                   onTap: () => context.pushRoute( | ||||
|                     SearchResultRoute( | ||||
|                       searchTerm: 'screenshots', | ||||
|                     ), | ||||
| @@ -214,7 +215,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                     Icons.photo_camera_front_outlined, | ||||
|                     color: categoryIconColor, | ||||
|                   ), | ||||
|                   onTap: () => context.autoPush( | ||||
|                   onTap: () => context.pushRoute( | ||||
|                     SearchResultRoute( | ||||
|                       searchTerm: 'selfies', | ||||
|                     ), | ||||
| @@ -228,7 +229,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                     Icons.play_circle_outline, | ||||
|                     color: categoryIconColor, | ||||
|                   ), | ||||
|                   onTap: () => context.autoPush(const AllVideosRoute()), | ||||
|                   onTap: () => context.pushRoute(const AllVideosRoute()), | ||||
|                 ), | ||||
|                 const CategoryDivider(), | ||||
|                 ListTile( | ||||
| @@ -240,7 +241,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                     Icons.motion_photos_on_outlined, | ||||
|                     color: categoryIconColor, | ||||
|                   ), | ||||
|                   onTap: () => context.autoPush(const AllMotionPhotosRoute()), | ||||
|                   onTap: () => context.pushRoute(const AllMotionPhotosRoute()), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -185,7 +186,7 @@ class SearchResultPage extends HookConsumerWidget { | ||||
|             if (isNewSearch.value) { | ||||
|               isNewSearch.value = false; | ||||
|             } else { | ||||
|               context.autoPop(true); | ||||
|               context.popRoute(true); | ||||
|             } | ||||
|           }, | ||||
|           icon: const Icon(Icons.arrow_back_ios_rounded), | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'dart:math' as math; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| @@ -210,8 +211,8 @@ class SharedLinkItem extends ConsumerWidget { | ||||
|               tapTargetSize: | ||||
|                   MaterialTapTargetSize.shrinkWrap, // the '2023' part | ||||
|             ), | ||||
|             onPressed: () => | ||||
|                 context.autoPush(SharedLinkEditRoute(existingLink: sharedLink)), | ||||
|             onPressed: () => context | ||||
|                 .pushRoute(SharedLinkEditRoute(existingLink: sharedLink)), | ||||
|           ), | ||||
|           IconButton( | ||||
|             splashRadius: 25, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| @@ -317,7 +318,7 @@ class SharedLinkEditPage extends HookConsumerWidget { | ||||
|               alignment: Alignment.bottomRight, | ||||
|               child: ElevatedButton( | ||||
|                 onPressed: () { | ||||
|                   context.autoPop(); | ||||
|                   context.popRoute(); | ||||
|                 }, | ||||
|                 child: const Text( | ||||
|                   "share_done", | ||||
| @@ -417,7 +418,7 @@ class SharedLinkEditPage extends HookConsumerWidget { | ||||
|             changeExpiry: changeExpiry, | ||||
|           ); | ||||
|       ref.invalidate(sharedLinksStateProvider); | ||||
|       context.autoPop(); | ||||
|       context.popRoute(); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -138,7 +139,7 @@ class TrashPage extends HookConsumerWidget { | ||||
|       return AppBar( | ||||
|         leading: IconButton( | ||||
|           onPressed: !selectionEnabledHook.value | ||||
|               ? () => context.autoPop() | ||||
|               ? () => context.popRoute() | ||||
|               : () { | ||||
|                   selectionEnabledHook.value = false; | ||||
|                   selection.value = {}; | ||||
|   | ||||
| @@ -340,18 +340,9 @@ class _$AppRouter extends RootStackRouter { | ||||
|       ); | ||||
|     }, | ||||
|     ActivitiesRoute.name: (routeData) { | ||||
|       final args = routeData.argsAs<ActivitiesRouteArgs>(); | ||||
|       return CustomPage<dynamic>( | ||||
|         routeData: routeData, | ||||
|         child: ActivitiesPage( | ||||
|           args.albumId, | ||||
|           appBarTitle: args.appBarTitle, | ||||
|           assetId: args.assetId, | ||||
|           withAssetThumbs: args.withAssetThumbs, | ||||
|           isOwner: args.isOwner, | ||||
|           isReadOnly: args.isReadOnly, | ||||
|           key: args.key, | ||||
|         ), | ||||
|         child: const ActivitiesPage(), | ||||
|         transitionsBuilder: TransitionsBuilders.slideLeft, | ||||
|         durationInMilliseconds: 200, | ||||
|         opaque: true, | ||||
| @@ -1587,63 +1578,16 @@ class SharedLinkEditRouteArgs { | ||||
|  | ||||
| /// generated route for | ||||
| /// [ActivitiesPage] | ||||
| class ActivitiesRoute extends PageRouteInfo<ActivitiesRouteArgs> { | ||||
|   ActivitiesRoute({ | ||||
|     required String albumId, | ||||
|     String appBarTitle = "", | ||||
|     String? assetId, | ||||
|     bool withAssetThumbs = true, | ||||
|     bool isOwner = false, | ||||
|     bool isReadOnly = false, | ||||
|     Key? key, | ||||
|   }) : super( | ||||
| class ActivitiesRoute extends PageRouteInfo<void> { | ||||
|   const ActivitiesRoute() | ||||
|       : super( | ||||
|           ActivitiesRoute.name, | ||||
|           path: '/activities-page', | ||||
|           args: ActivitiesRouteArgs( | ||||
|             albumId: albumId, | ||||
|             appBarTitle: appBarTitle, | ||||
|             assetId: assetId, | ||||
|             withAssetThumbs: withAssetThumbs, | ||||
|             isOwner: isOwner, | ||||
|             isReadOnly: isReadOnly, | ||||
|             key: key, | ||||
|           ), | ||||
|         ); | ||||
|  | ||||
|   static const String name = 'ActivitiesRoute'; | ||||
| } | ||||
|  | ||||
| class ActivitiesRouteArgs { | ||||
|   const ActivitiesRouteArgs({ | ||||
|     required this.albumId, | ||||
|     this.appBarTitle = "", | ||||
|     this.assetId, | ||||
|     this.withAssetThumbs = true, | ||||
|     this.isOwner = false, | ||||
|     this.isReadOnly = false, | ||||
|     this.key, | ||||
|   }); | ||||
|  | ||||
|   final String albumId; | ||||
|  | ||||
|   final String appBarTitle; | ||||
|  | ||||
|   final String? assetId; | ||||
|  | ||||
|   final bool withAssetThumbs; | ||||
|  | ||||
|   final bool isOwner; | ||||
|  | ||||
|   final bool isReadOnly; | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'ActivitiesRouteArgs{albumId: $albumId, appBarTitle: $appBarTitle, assetId: $assetId, withAssetThumbs: $withAssetThumbs, isOwner: $isOwner, isReadOnly: $isReadOnly, key: $key}'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [MapLocationPickerPage] | ||||
| class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> { | ||||
|   | ||||
| @@ -51,6 +51,21 @@ class User { | ||||
|         avatarColor = dto.avatarColor.toAvatarColor(), | ||||
|         inTimeline = dto.inTimeline ?? false; | ||||
|  | ||||
|   /// Base user dto used where the complete user object is not required | ||||
|   User.fromSimpleUserDto(UserDto dto) | ||||
|       : id = dto.id, | ||||
|         email = dto.email, | ||||
|         name = dto.name, | ||||
|         profileImagePath = dto.profileImagePath, | ||||
|         avatarColor = dto.avatarColor.toAvatarColor(), | ||||
|         // Fill the remaining fields with placeholders | ||||
|         isAdmin = false, | ||||
|         inTimeline = false, | ||||
|         memoryEnabled = false, | ||||
|         isPartnerSharedBy = false, | ||||
|         isPartnerSharedWith = false, | ||||
|         updatedAt = DateTime.now(); | ||||
|  | ||||
|   @Index(unique: true, replace: false, type: IndexType.hash) | ||||
|   String id; | ||||
|   DateTime updatedAt; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -90,7 +91,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { | ||||
|       return buildActionButton( | ||||
|         Icons.settings_rounded, | ||||
|         "profile_drawer_settings", | ||||
|         () => context.autoPush(const SettingsRoute()), | ||||
|         () => context.pushRoute(const SettingsRoute()), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
| @@ -98,7 +99,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { | ||||
|       return buildActionButton( | ||||
|         Icons.assignment_outlined, | ||||
|         "profile_drawer_app_logs", | ||||
|         () => context.autoPush(const AppLogRoute()), | ||||
|         () => context.pushRoute(const AppLogRoute()), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
| @@ -121,7 +122,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { | ||||
|                   ref.watch(backupProvider.notifier).cancelBackup(); | ||||
|                   ref.watch(assetProvider.notifier).clearAllAsset(); | ||||
|                   ref.watch(websocketProvider.notifier).disconnect(); | ||||
|                   context.autoReplace(const LoginRoute()); | ||||
|                   context.replaceRoute(const LoginRoute()); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:fluttertoast/fluttertoast.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/services/album.service.dart'; | ||||
| @@ -158,7 +158,7 @@ class MultiselectGrid extends HookConsumerWidget { | ||||
|         final ids = | ||||
|             remoteSelection(errorMessage: "home_page_share_err_local".tr()) | ||||
|                 .map((e) => e.remoteId!); | ||||
|         context.autoPush(SharedLinkEditRoute(assetsList: ids.toList())); | ||||
|         context.pushRoute(SharedLinkEditRoute(assetsList: ids.toList())); | ||||
|       } | ||||
|       processing.value = false; | ||||
|       selectionEnabledHook.value = false; | ||||
| @@ -301,7 +301,7 @@ class MultiselectGrid extends HookConsumerWidget { | ||||
|           ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); | ||||
|           selectionEnabledHook.value = false; | ||||
|  | ||||
|           context.autoPush(AlbumViewerRoute(albumId: result.id)); | ||||
|           context.pushRoute(AlbumViewerRoute(albumId: result.id)); | ||||
|         } | ||||
|       } finally { | ||||
|         processing.value = false; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| @@ -106,7 +107,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { | ||||
|       final badgeBackground = isDarkTheme ? Colors.blueGrey[800] : Colors.white; | ||||
|  | ||||
|       return InkWell( | ||||
|         onTap: () => context.autoPush(const BackupControllerRoute()), | ||||
|         onTap: () => context.pushRoute(const BackupControllerRoute()), | ||||
|         borderRadius: BorderRadius.circular(12), | ||||
|         child: Badge( | ||||
|           label: Container( | ||||
|   | ||||
| @@ -162,6 +162,19 @@ class ImmichImage extends StatelessWidget { | ||||
|         headers: authHeader, | ||||
|       ); | ||||
|  | ||||
|   /// TODO: refactor image providers to separate class | ||||
|   static CachedNetworkImageProvider remoteThumbnailProviderForId( | ||||
|     String assetId, { | ||||
|     api.ThumbnailFormat type = api.ThumbnailFormat.WEBP, | ||||
|   }) => | ||||
|       CachedNetworkImageProvider( | ||||
|         getThumbnailUrlForRemoteId(assetId, type: type), | ||||
|         cacheKey: getThumbnailCacheKeyForRemoteId(assetId, type: type), | ||||
|         headers: { | ||||
|           "Authorization": 'Bearer ${Store.get(StoreKey.accessToken)}', | ||||
|         }, | ||||
|       ); | ||||
|  | ||||
|   /// Precaches this asset for instant load the next time it is shown | ||||
|   static Future<void> precacheAsset( | ||||
|     Asset asset, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| @@ -97,7 +98,7 @@ class _LocationPicker extends HookWidget { | ||||
|           zoom: 6, | ||||
|           showAttribution: false, | ||||
|           onTap: (p0, p1) async { | ||||
|             final newLatLng = await context.autoPush<LatLng?>( | ||||
|             final newLatLng = await context.pushRoute<LatLng?>( | ||||
|               MapLocationPickerRoute(initialLatLng: latlng), | ||||
|             ); | ||||
|             if (newLatLng != null) { | ||||
|   | ||||
| @@ -5,8 +5,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| // Error widget to be used in Scaffold when an AsyncError is received | ||||
| class ScaffoldErrorBody extends StatelessWidget { | ||||
|   final bool withIcon; | ||||
|   final String? errorMsg; | ||||
|  | ||||
|   const ScaffoldErrorBody({super.key, this.withIcon = true}); | ||||
|   const ScaffoldErrorBody({super.key, this.withIcon = true, this.errorMsg}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -30,6 +31,15 @@ class ScaffoldErrorBody extends StatelessWidget { | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         if (withIcon && errorMsg != null) | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.all(20), | ||||
|             child: Text( | ||||
|               errorMsg!, | ||||
|               style: context.textTheme.displaySmall, | ||||
|               textAlign: TextAlign.center, | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -103,7 +104,7 @@ class AppLogPage extends HookConsumerWidget { | ||||
|         ], | ||||
|         leading: IconButton( | ||||
|           onPressed: () { | ||||
|             context.autoPop(); | ||||
|             context.popRoute(); | ||||
|           }, | ||||
|           icon: const Icon( | ||||
|             Icons.arrow_back_ios_new_rounded, | ||||
| @@ -123,7 +124,7 @@ class AppLogPage extends HookConsumerWidget { | ||||
|         itemBuilder: (context, index) { | ||||
|           var logMessage = logMessages.value[index]; | ||||
|           return ListTile( | ||||
|             onTap: () => context.autoPush( | ||||
|             onTap: () => context.pushRoute( | ||||
|               AppLogDetailRoute( | ||||
|                 logMessage: logMessage, | ||||
|               ), | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| @@ -57,14 +57,14 @@ class SplashScreenPage extends HookConsumerWidget { | ||||
|             stackTrace, | ||||
|           ); | ||||
|  | ||||
|           context.autoPush(const LoginRoute()); | ||||
|           context.pushRoute(const LoginRoute()); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // If the device is offline and there is a currentUser stored locallly | ||||
|       // Proceed into the app | ||||
|       if (deviceIsOffline && Store.tryGet(StoreKey.currentUser) != null) { | ||||
|         context.autoReplace(const TabControllerRoute()); | ||||
|         context.replaceRoute(const TabControllerRoute()); | ||||
|       } else if (isSuccess) { | ||||
|         // If device was able to login through the internet successfully | ||||
|         final hasPermission = | ||||
| @@ -73,10 +73,10 @@ class SplashScreenPage extends HookConsumerWidget { | ||||
|           // Resume backup (if enable) then navigate | ||||
|           ref.watch(backupProvider.notifier).resumeBackup(); | ||||
|         } | ||||
|         context.autoReplace(const TabControllerRoute()); | ||||
|         context.replaceRoute(const TabControllerRoute()); | ||||
|       } else { | ||||
|         // User was unable to login through either offline or online methods | ||||
|         context.autoReplace(const LoginRoute()); | ||||
|         context.replaceRoute(const LoginRoute()); | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -85,7 +85,7 @@ class SplashScreenPage extends HookConsumerWidget { | ||||
|         if (serverUrl != null && accessToken != null) { | ||||
|           performLoggingIn(); | ||||
|         } else { | ||||
|           context.autoReplace(const LoginRoute()); | ||||
|           context.replaceRoute(const LoginRoute()); | ||||
|         } | ||||
|         return null; | ||||
|       }, | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/test/fixtures/album.stub.dart
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								mobile/test/fixtures/album.stub.dart
									
									
									
									
										vendored
									
									
								
							| @@ -50,5 +50,8 @@ final class AlbumStub { | ||||
|     activityEnabled: false, | ||||
|     startDate: DateTime(2019), | ||||
|     endDate: DateTime(2020), | ||||
|   )..assets.addAll([AssetStub.image1, AssetStub.image2]); | ||||
|   ) | ||||
|     ..assets.addAll([AssetStub.image1, AssetStub.image2]) | ||||
|     ..activityEnabled = true | ||||
|     ..owner.value = UserStub.admin; | ||||
| } | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/test/fixtures/asset.stub.dart
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								mobile/test/fixtures/asset.stub.dart
									
									
									
									
										vendored
									
									
								
							| @@ -6,6 +6,7 @@ final class AssetStub { | ||||
|   static final image1 = Asset( | ||||
|     checksum: "image1-checksum", | ||||
|     localId: "image1", | ||||
|     remoteId: 'image1-remote', | ||||
|     ownerId: 1, | ||||
|     fileCreatedAt: DateTime.now(), | ||||
|     fileModifiedAt: DateTime.now(), | ||||
| @@ -22,6 +23,7 @@ final class AssetStub { | ||||
|   static final image2 = Asset( | ||||
|     checksum: "image2-checksum", | ||||
|     localId: "image2", | ||||
|     remoteId: 'image2-remote', | ||||
|     ownerId: 1, | ||||
|     fileCreatedAt: DateTime(2000), | ||||
|     fileModifiedAt: DateTime(2010), | ||||
|   | ||||
							
								
								
									
										14
									
								
								mobile/test/fixtures/user.stub.dart
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								mobile/test/fixtures/user.stub.dart
									
									
									
									
										vendored
									
									
								
							| @@ -8,6 +8,8 @@ final class UserStub { | ||||
|     updatedAt: DateTime(2021), | ||||
|     email: "admin@test.com", | ||||
|     name: "admin", | ||||
|     avatarColor: AvatarColorEnum.green, | ||||
|     profileImagePath: '', | ||||
|     isAdmin: true, | ||||
|   ); | ||||
|  | ||||
| @@ -16,6 +18,18 @@ final class UserStub { | ||||
|     updatedAt: DateTime(2022), | ||||
|     email: "user1@test.com", | ||||
|     name: "user1", | ||||
|     avatarColor: AvatarColorEnum.red, | ||||
|     profileImagePath: '', | ||||
|     isAdmin: false, | ||||
|   ); | ||||
|  | ||||
|   static final user2 = User( | ||||
|     id: "user2", | ||||
|     updatedAt: DateTime(2023), | ||||
|     email: "user2@test.com", | ||||
|     name: "user2", | ||||
|     avatarColor: AvatarColorEnum.primary, | ||||
|     profileImagePath: '', | ||||
|     isAdmin: false, | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										67
									
								
								mobile/test/mock_http_override.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								mobile/test/mock_http_override.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:immich_mobile/shared/ui/transparent_image.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
|  | ||||
| /// Mocks the http client to always return a transparent image for all the requests. Only useful in widget | ||||
| /// tests to return network images | ||||
| class MockHttpOverrides extends HttpOverrides { | ||||
|   @override | ||||
|   HttpClient createHttpClient(SecurityContext? context) { | ||||
|     final client = _MockHttpClient(); | ||||
|     final request = _MockHttpClientRequest(); | ||||
|     final response = _MockHttpClientResponse(); | ||||
|     final headers = _MockHttpHeaders(); | ||||
|  | ||||
|     // Client mocks | ||||
|     when(() => client.autoUncompress).thenReturn(true); | ||||
|  | ||||
|     // Request mocks | ||||
|     when(() => request.headers).thenAnswer((_) => headers); | ||||
|     when(() => request.close()) | ||||
|         .thenAnswer((_) => Future<HttpClientResponse>.value(response)); | ||||
|  | ||||
|     // Response mocks | ||||
|     when(() => response.statusCode).thenReturn(HttpStatus.ok); | ||||
|     when(() => response.compressionState) | ||||
|         .thenReturn(HttpClientResponseCompressionState.decompressed); | ||||
|     when(() => response.contentLength) | ||||
|         .thenAnswer((_) => kTransparentImage.length); | ||||
|     when( | ||||
|       () => response.listen( | ||||
|         captureAny(), | ||||
|         cancelOnError: captureAny(named: 'cancelOnError'), | ||||
|         onDone: captureAny(named: 'onDone'), | ||||
|         onError: captureAny(named: 'onError'), | ||||
|       ), | ||||
|     ).thenAnswer((invocation) { | ||||
|       final onData = | ||||
|           invocation.positionalArguments[0] as void Function(List<int>); | ||||
|  | ||||
|       final onDone = invocation.namedArguments[#onDone] as void Function(); | ||||
|  | ||||
|       final onError = invocation.namedArguments[#onError] as void | ||||
|           Function(Object, [StackTrace]); | ||||
|  | ||||
|       final cancelOnError = invocation.namedArguments[#cancelOnError] as bool; | ||||
|  | ||||
|       return Stream<List<int>>.fromIterable([kTransparentImage.toList()]) | ||||
|           .listen( | ||||
|         onData, | ||||
|         onDone: onDone, | ||||
|         onError: onError, | ||||
|         cancelOnError: cancelOnError, | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     return client; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _MockHttpClient extends Mock implements HttpClient {} | ||||
|  | ||||
| class _MockHttpClientRequest extends Mock implements HttpClientRequest {} | ||||
|  | ||||
| class _MockHttpClientResponse extends Mock implements HttpClientResponse {} | ||||
|  | ||||
| class _MockHttpHeaders extends Mock implements HttpHeaders {} | ||||
| @@ -1,9 +0,0 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; | ||||
| import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
|  | ||||
| class AppSettingsServiceMock with Mock implements AppSettingsService {} | ||||
|  | ||||
| Override getAppSettingsServiceMock(AppSettingsService service) => | ||||
|     appSettingsServiceProvider.overrideWith((ref) => service); | ||||
							
								
								
									
										250
									
								
								mobile/test/modules/activity/activities_page_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								mobile/test/modules/activity/activities_page_test.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,250 @@ | ||||
| @Tags(['widget']) | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| import 'package:immich_mobile/modules/activities/models/activity.model.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/views/activities_page.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/activity_text_field.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/dismissible_activity.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/current_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/shared/providers/user.provider.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| import '../../fixtures/album.stub.dart'; | ||||
| import '../../fixtures/asset.stub.dart'; | ||||
| import '../../fixtures/user.stub.dart'; | ||||
| import '../../test_utils.dart'; | ||||
| import '../../widget_tester_extensions.dart'; | ||||
| import '../asset_viewer/asset_viewer_mocks.dart'; | ||||
| import '../album/album_mocks.dart'; | ||||
| import '../shared/shared_mocks.dart'; | ||||
| import 'activity_mocks.dart'; | ||||
|  | ||||
| final _activities = [ | ||||
|   Activity( | ||||
|     id: '1', | ||||
|     createdAt: DateTime(100), | ||||
|     type: ActivityType.comment, | ||||
|     comment: 'First Activity', | ||||
|     assetId: 'asset-2', | ||||
|     user: UserStub.admin, | ||||
|   ), | ||||
|   Activity( | ||||
|     id: '2', | ||||
|     createdAt: DateTime(200), | ||||
|     type: ActivityType.comment, | ||||
|     comment: 'Second Activity', | ||||
|     user: UserStub.user1, | ||||
|   ), | ||||
|   Activity( | ||||
|     id: '3', | ||||
|     createdAt: DateTime(300), | ||||
|     type: ActivityType.like, | ||||
|     assetId: 'asset-1', | ||||
|     user: UserStub.user2, | ||||
|   ), | ||||
|   Activity( | ||||
|     id: '4', | ||||
|     createdAt: DateTime(400), | ||||
|     type: ActivityType.like, | ||||
|     user: UserStub.user1, | ||||
|   ), | ||||
| ]; | ||||
|  | ||||
| void main() { | ||||
|   late MockAlbumActivity activityMock; | ||||
|   late MockCurrentAlbumProvider mockCurrentAlbumProvider; | ||||
|   late MockCurrentAssetProvider mockCurrentAssetProvider; | ||||
|   late List<Override> overrides; | ||||
|   late Isar db; | ||||
|  | ||||
|   setUpAll(() async { | ||||
|     TestUtils.init(); | ||||
|     db = await TestUtils.initIsar(); | ||||
|     Store.init(db); | ||||
|     Store.put(StoreKey.currentUser, UserStub.admin); | ||||
|     Store.put(StoreKey.serverEndpoint, ''); | ||||
|     Store.put(StoreKey.accessToken, ''); | ||||
|   }); | ||||
|  | ||||
|   setUp(() async { | ||||
|     mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); | ||||
|     mockCurrentAssetProvider = MockCurrentAssetProvider(AssetStub.image1); | ||||
|     activityMock = MockAlbumActivity(_activities); | ||||
|     overrides = [ | ||||
|       albumActivityProvider( | ||||
|         AlbumStub.twoAsset.remoteId!, | ||||
|         AssetStub.image1.remoteId!, | ||||
|       ).overrideWith(() => activityMock), | ||||
|       currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), | ||||
|       currentAssetProvider.overrideWith(() => mockCurrentAssetProvider), | ||||
|     ]; | ||||
|  | ||||
|     await db.writeTxn(() async { | ||||
|       await db.clear(); | ||||
|       // Save all assets | ||||
|       await db.users.put(UserStub.admin); | ||||
|       await db.assets.putAll([AssetStub.image1, AssetStub.image2]); | ||||
|       await db.albums.put(AlbumStub.twoAsset); | ||||
|       await AlbumStub.twoAsset.owner.save(); | ||||
|       await AlbumStub.twoAsset.assets.save(); | ||||
|     }); | ||||
|     expect(db.albums.countSync(), 1); | ||||
|     expect(db.assets.countSync(), 2); | ||||
|     expect(db.users.countSync(), 1); | ||||
|   }); | ||||
|  | ||||
|   group("App bar", () { | ||||
|     testWidgets( | ||||
|       "No title when currentAsset != null", | ||||
|       (tester) async { | ||||
|         await tester.pumpConsumerWidget( | ||||
|           const ActivitiesPage(), | ||||
|           overrides: overrides, | ||||
|         ); | ||||
|  | ||||
|         final listTile = tester.widget<AppBar>(find.byType(AppBar)); | ||||
|         expect(listTile.title, isNull); | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     testWidgets( | ||||
|       "Album name as title when currentAsset == null", | ||||
|       (tester) async { | ||||
|         await tester.pumpConsumerWidget( | ||||
|           const ActivitiesPage(), | ||||
|           overrides: overrides, | ||||
|         ); | ||||
|         await tester.pumpAndSettle(); | ||||
|  | ||||
|         mockCurrentAssetProvider.state = null; | ||||
|         await tester.pumpAndSettle(); | ||||
|  | ||||
|         expect(find.text(AlbumStub.twoAsset.name), findsOneWidget); | ||||
|         final listTile = tester.widget<AppBar>(find.byType(AppBar)); | ||||
|         expect(listTile.title, isNotNull); | ||||
|       }, | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   group("Body", () { | ||||
|     testWidgets( | ||||
|       "Contains a stack with Activity List and Activity Input", | ||||
|       (tester) async { | ||||
|         await tester.pumpConsumerWidget( | ||||
|           const ActivitiesPage(), | ||||
|           overrides: overrides, | ||||
|         ); | ||||
|         await tester.pumpAndSettle(); | ||||
|  | ||||
|         expect( | ||||
|           find.descendant( | ||||
|             of: find.byType(Stack), | ||||
|             matching: find.byType(ActivityTextField), | ||||
|           ), | ||||
|           findsOneWidget, | ||||
|         ); | ||||
|  | ||||
|         expect( | ||||
|           find.descendant( | ||||
|             of: find.byType(Stack), | ||||
|             matching: find.byType(ListView), | ||||
|           ), | ||||
|           findsOneWidget, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     testWidgets( | ||||
|       "List Contains all dismissible activities", | ||||
|       (tester) async { | ||||
|         await tester.pumpConsumerWidget( | ||||
|           const ActivitiesPage(), | ||||
|           overrides: overrides, | ||||
|         ); | ||||
|         await tester.pumpAndSettle(); | ||||
|  | ||||
|         final listFinder = find.descendant( | ||||
|           of: find.byType(Stack), | ||||
|           matching: find.byType(ListView), | ||||
|         ); | ||||
|         final listChildren = find.descendant( | ||||
|           of: listFinder, | ||||
|           matching: find.byType(DismissibleActivity), | ||||
|         ); | ||||
|         expect(listChildren, findsNWidgets(_activities.length)); | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     testWidgets( | ||||
|       "Submitting text input adds a comment with the text", | ||||
|       (tester) async { | ||||
|         await tester.pumpConsumerWidget( | ||||
|           const ActivitiesPage(), | ||||
|           overrides: overrides, | ||||
|         ); | ||||
|         await tester.pumpAndSettle(); | ||||
|  | ||||
|         when(() => activityMock.addComment(any())) | ||||
|             .thenAnswer((_) => Future.value()); | ||||
|  | ||||
|         final textField = find.byType(TextField); | ||||
|         await tester.enterText(textField, 'Test comment'); | ||||
|         await tester.testTextInput.receiveAction(TextInputAction.done); | ||||
|  | ||||
|         verify(() => activityMock.addComment('Test comment')); | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     testWidgets( | ||||
|       "Owner can remove all activities", | ||||
|       (tester) async { | ||||
|         await tester.pumpConsumerWidget( | ||||
|           const ActivitiesPage(), | ||||
|           overrides: overrides, | ||||
|         ); | ||||
|         await tester.pumpAndSettle(); | ||||
|  | ||||
|         final deletableActivityFinder = find.byWidgetPredicate( | ||||
|           (widget) => widget is DismissibleActivity && widget.onDismiss != null, | ||||
|         ); | ||||
|         expect(deletableActivityFinder, findsNWidgets(_activities.length)); | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     testWidgets( | ||||
|       "Non-Owner can remove only their activities", | ||||
|       (tester) async { | ||||
|         final mockCurrentUser = MockCurrentUserProvider(); | ||||
|  | ||||
|         await tester.pumpConsumerWidget( | ||||
|           const ActivitiesPage(), | ||||
|           overrides: [ | ||||
|             ...overrides, | ||||
|             currentUserProvider.overrideWith((ref) => mockCurrentUser), | ||||
|           ], | ||||
|         ); | ||||
|         mockCurrentUser.state = UserStub.user1; | ||||
|         await tester.pumpAndSettle(); | ||||
|  | ||||
|         final deletableActivityFinder = find.byWidgetPredicate( | ||||
|           (widget) => widget is DismissibleActivity && widget.onDismiss != null, | ||||
|         ); | ||||
|         expect( | ||||
|           deletableActivityFinder, | ||||
|           findsNWidgets( | ||||
|             _activities.where((a) => a.user == UserStub.user1).length, | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										23
									
								
								mobile/test/modules/activity/activity_mocks.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								mobile/test/modules/activity/activity_mocks.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import 'package:immich_mobile/modules/activities/models/activity.model.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/services/activity.service.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
|  | ||||
| class ActivityServiceMock extends Mock implements ActivityService {} | ||||
|  | ||||
| class MockAlbumActivity extends AlbumActivityInternal | ||||
|     with Mock | ||||
|     implements AlbumActivity { | ||||
|   List<Activity>? initActivities; | ||||
|   MockAlbumActivity([this.initActivities]); | ||||
|  | ||||
|   @override | ||||
|   Future<List<Activity>> build(String albumId, [String? assetId]) async { | ||||
|     return initActivities ?? []; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ActivityStatisticsMock extends ActivityStatisticsInternal | ||||
|     with Mock | ||||
|     implements ActivityStatistics {} | ||||
							
								
								
									
										353
									
								
								mobile/test/modules/activity/activity_provider_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										353
									
								
								mobile/test/modules/activity/activity_provider_test.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,353 @@ | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/activities/models/activity.model.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
|  | ||||
| import '../../fixtures/user.stub.dart'; | ||||
| import '../../test_utils.dart'; | ||||
| import 'activity_mocks.dart'; | ||||
|  | ||||
| final _activities = [ | ||||
|   Activity( | ||||
|     id: '1', | ||||
|     createdAt: DateTime(100), | ||||
|     type: ActivityType.comment, | ||||
|     comment: 'First Activity', | ||||
|     assetId: 'asset-2', | ||||
|     user: UserStub.admin, | ||||
|   ), | ||||
|   Activity( | ||||
|     id: '2', | ||||
|     createdAt: DateTime(200), | ||||
|     type: ActivityType.comment, | ||||
|     comment: 'Second Activity', | ||||
|     user: UserStub.user1, | ||||
|   ), | ||||
|   Activity( | ||||
|     id: '3', | ||||
|     createdAt: DateTime(300), | ||||
|     type: ActivityType.like, | ||||
|     assetId: 'asset-1', | ||||
|     user: UserStub.admin, | ||||
|   ), | ||||
|   Activity( | ||||
|     id: '4', | ||||
|     createdAt: DateTime(400), | ||||
|     type: ActivityType.like, | ||||
|     user: UserStub.user1, | ||||
|   ), | ||||
| ]; | ||||
|  | ||||
| void main() { | ||||
|   late ActivityServiceMock activityMock; | ||||
|   late ActivityStatisticsMock activityStatisticsMock; | ||||
|   late ProviderContainer container; | ||||
|   late AlbumActivityProvider provider; | ||||
|   late ListenerMock<AsyncValue<List<Activity>>> listener; | ||||
|  | ||||
|   setUpAll(() { | ||||
|     registerFallbackValue(AsyncData<List<Activity>>([..._activities])); | ||||
|   }); | ||||
|  | ||||
|   setUp(() async { | ||||
|     activityMock = ActivityServiceMock(); | ||||
|     activityStatisticsMock = ActivityStatisticsMock(); | ||||
|     container = TestUtils.createContainer( | ||||
|       overrides: [ | ||||
|         activityServiceProvider.overrideWith((ref) => activityMock), | ||||
|         activityStatisticsProvider('test-album', 'test-asset') | ||||
|             .overrideWith(() => activityStatisticsMock), | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     // Mock values | ||||
|     when( | ||||
|       () => activityMock.getAllActivities('test-album', assetId: 'test-asset'), | ||||
|     ).thenAnswer((_) async => [..._activities]); | ||||
|  | ||||
|     // Init and wait for providers future to complete | ||||
|     provider = albumActivityProvider('test-album', 'test-asset'); | ||||
|     listener = ListenerMock(); | ||||
|     container.listen( | ||||
|       provider, | ||||
|       listener, | ||||
|       fireImmediately: true, | ||||
|     ); | ||||
|  | ||||
|     await container.read(provider.future); | ||||
|   }); | ||||
|  | ||||
|   test('Returns a list of activity', () async { | ||||
|     verifyInOrder([ | ||||
|       () => listener.call(null, const AsyncLoading()), | ||||
|       () => listener.call( | ||||
|             const AsyncLoading(), | ||||
|             any( | ||||
|               that: allOf( | ||||
|                 [ | ||||
|                   isA<AsyncData<List<Activity>>>(), | ||||
|                   predicate( | ||||
|                     (AsyncData<List<Activity>> ad) => | ||||
|                         ad.requireValue.every((e) => _activities.contains(e)), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|     ]); | ||||
|  | ||||
|     verifyNoMoreInteractions(listener); | ||||
|   }); | ||||
|  | ||||
|   group('addLike()', () { | ||||
|     test('Like successfully added', () async { | ||||
|       final like = Activity( | ||||
|         id: '5', | ||||
|         createdAt: DateTime(2023), | ||||
|         type: ActivityType.like, | ||||
|         user: UserStub.admin, | ||||
|       ); | ||||
|  | ||||
|       when( | ||||
|         () => activityMock.addActivity( | ||||
|           'test-album', | ||||
|           ActivityType.like, | ||||
|           assetId: 'test-asset', | ||||
|         ), | ||||
|       ).thenAnswer((_) async => AsyncData(like)); | ||||
|  | ||||
|       await container.read(provider.notifier).addLike(); | ||||
|  | ||||
|       verify( | ||||
|         () => activityMock.addActivity( | ||||
|           'test-album', | ||||
|           ActivityType.like, | ||||
|           assetId: 'test-asset', | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       final activities = await container.read(provider.future); | ||||
|       expect(activities, hasLength(5)); | ||||
|       expect(activities, contains(like)); | ||||
|  | ||||
|       // Never bump activity count for new likes | ||||
|       verifyNever(() => activityStatisticsMock.addActivity()); | ||||
|     }); | ||||
|  | ||||
|     test('Like failed', () async { | ||||
|       final like = Activity( | ||||
|         id: '5', | ||||
|         createdAt: DateTime(2023), | ||||
|         type: ActivityType.like, | ||||
|         user: UserStub.admin, | ||||
|       ); | ||||
|       when( | ||||
|         () => activityMock.addActivity( | ||||
|           'test-album', | ||||
|           ActivityType.like, | ||||
|           assetId: 'test-asset', | ||||
|         ), | ||||
|       ).thenAnswer( | ||||
|         (_) async => AsyncError(Exception('Mock'), StackTrace.current), | ||||
|       ); | ||||
|  | ||||
|       await container.read(provider.notifier).addLike(); | ||||
|  | ||||
|       verify( | ||||
|         () => activityMock.addActivity( | ||||
|           'test-album', | ||||
|           ActivityType.like, | ||||
|           assetId: 'test-asset', | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       final activities = await container.read(provider.future); | ||||
|       expect(activities, hasLength(4)); | ||||
|       expect(activities, isNot(contains(like))); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   group('removeActivity()', () { | ||||
|     test('Like successfully removed', () async { | ||||
|       when(() => activityMock.removeActivity('3')) | ||||
|           .thenAnswer((_) async => true); | ||||
|  | ||||
|       await container.read(provider.notifier).removeActivity('3'); | ||||
|  | ||||
|       verify( | ||||
|         () => activityMock.removeActivity('3'), | ||||
|       ); | ||||
|  | ||||
|       final activities = await container.read(provider.future); | ||||
|       expect(activities, hasLength(3)); | ||||
|       expect( | ||||
|         activities, | ||||
|         isNot(anyElement(predicate((Activity a) => a.id == '3'))), | ||||
|       ); | ||||
|  | ||||
|       verifyNever(() => activityStatisticsMock.removeActivity()); | ||||
|     }); | ||||
|  | ||||
|     test('Remove Like failed', () async { | ||||
|       when(() => activityMock.removeActivity('3')) | ||||
|           .thenAnswer((_) async => false); | ||||
|  | ||||
|       await container.read(provider.notifier).removeActivity('3'); | ||||
|  | ||||
|       final activities = await container.read(provider.future); | ||||
|       expect(activities, hasLength(4)); | ||||
|       expect( | ||||
|         activities, | ||||
|         anyElement(predicate((Activity a) => a.id == '3')), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     test('Comment successfully removed', () async { | ||||
|       when(() => activityMock.removeActivity('1')) | ||||
|           .thenAnswer((_) async => true); | ||||
|  | ||||
|       await container.read(provider.notifier).removeActivity('1'); | ||||
|  | ||||
|       final activities = await container.read(provider.future); | ||||
|       expect( | ||||
|         activities, | ||||
|         isNot(anyElement(predicate((Activity a) => a.id == '1'))), | ||||
|       ); | ||||
|  | ||||
|       verify(() => activityStatisticsMock.removeActivity()); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   group('addComment()', () { | ||||
|     late ActivityStatisticsMock albumActivityStatisticsMock; | ||||
|  | ||||
|     setUp(() { | ||||
|       albumActivityStatisticsMock = ActivityStatisticsMock(); | ||||
|       container = TestUtils.createContainer( | ||||
|         overrides: [ | ||||
|           activityServiceProvider.overrideWith((ref) => activityMock), | ||||
|           activityStatisticsProvider('test-album', 'test-asset') | ||||
|               .overrideWith(() => activityStatisticsMock), | ||||
|           activityStatisticsProvider('test-album') | ||||
|               .overrideWith(() => albumActivityStatisticsMock), | ||||
|         ], | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     test('Comment successfully added', () async { | ||||
|       final comment = Activity( | ||||
|         id: '5', | ||||
|         createdAt: DateTime(2023), | ||||
|         type: ActivityType.comment, | ||||
|         user: UserStub.admin, | ||||
|         comment: 'Test-Comment', | ||||
|         assetId: 'test-asset', | ||||
|       ); | ||||
|  | ||||
|       when( | ||||
|         () => activityMock.addActivity( | ||||
|           'test-album', | ||||
|           ActivityType.comment, | ||||
|           assetId: 'test-asset', | ||||
|           comment: 'Test-Comment', | ||||
|         ), | ||||
|       ).thenAnswer((_) async => AsyncData(comment)); | ||||
|       when(() => activityStatisticsMock.build('test-album', 'test-asset')) | ||||
|           .thenReturn(4); | ||||
|       when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); | ||||
|  | ||||
|       await container.read(provider.notifier).addComment('Test-Comment'); | ||||
|  | ||||
|       verify( | ||||
|         () => activityMock.addActivity( | ||||
|           'test-album', | ||||
|           ActivityType.comment, | ||||
|           assetId: 'test-asset', | ||||
|           comment: 'Test-Comment', | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       final activities = await container.read(provider.future); | ||||
|       expect(activities, hasLength(5)); | ||||
|       expect(activities, contains(comment)); | ||||
|  | ||||
|       verify(() => activityStatisticsMock.addActivity()); | ||||
|       verify(() => albumActivityStatisticsMock.addActivity()); | ||||
|     }); | ||||
|  | ||||
|     test('Comment successfully added without assetId', () async { | ||||
|       final comment = Activity( | ||||
|         id: '5', | ||||
|         createdAt: DateTime(2023), | ||||
|         type: ActivityType.comment, | ||||
|         user: UserStub.admin, | ||||
|         assetId: 'test-asset', | ||||
|         comment: 'Test-Comment', | ||||
|       ); | ||||
|  | ||||
|       when( | ||||
|         () => activityMock.addActivity( | ||||
|           'test-album', | ||||
|           ActivityType.comment, | ||||
|           comment: 'Test-Comment', | ||||
|         ), | ||||
|       ).thenAnswer((_) async => AsyncData(comment)); | ||||
|       when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2); | ||||
|       when(() => activityMock.getAllActivities('test-album')) | ||||
|           .thenAnswer((_) async => [..._activities]); | ||||
|  | ||||
|       final albumProvider = albumActivityProvider('test-album'); | ||||
|       await container.read(albumProvider.notifier).addComment('Test-Comment'); | ||||
|  | ||||
|       verify( | ||||
|         () => activityMock.addActivity( | ||||
|           'test-album', | ||||
|           ActivityType.comment, | ||||
|           assetId: null, | ||||
|           comment: 'Test-Comment', | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       final activities = await container.read(albumProvider.future); | ||||
|       expect(activities, hasLength(5)); | ||||
|       expect(activities, contains(comment)); | ||||
|  | ||||
|       verifyNever(() => activityStatisticsMock.addActivity()); | ||||
|       verify(() => albumActivityStatisticsMock.addActivity()); | ||||
|     }); | ||||
|  | ||||
|     test('Comment failed', () async { | ||||
|       final comment = Activity( | ||||
|         id: '5', | ||||
|         createdAt: DateTime(2023), | ||||
|         type: ActivityType.comment, | ||||
|         user: UserStub.admin, | ||||
|         comment: 'Test-Comment', | ||||
|         assetId: 'test-asset', | ||||
|       ); | ||||
|  | ||||
|       when( | ||||
|         () => activityMock.addActivity( | ||||
|           'test-album', | ||||
|           ActivityType.comment, | ||||
|           assetId: 'test-asset', | ||||
|           comment: 'Test-Comment', | ||||
|         ), | ||||
|       ).thenAnswer( | ||||
|         (_) async => AsyncError(Exception('Error'), StackTrace.current), | ||||
|       ); | ||||
|  | ||||
|       await container.read(provider.notifier).addComment('Test-Comment'); | ||||
|  | ||||
|       final activities = await container.read(provider.future); | ||||
|       expect(activities, hasLength(4)); | ||||
|       expect(activities, isNot(contains(comment))); | ||||
|  | ||||
|       verifyNever(() => activityStatisticsMock.addActivity()); | ||||
|       verifyNever(() => albumActivityStatisticsMock.addActivity()); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| @@ -0,0 +1,91 @@ | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
|  | ||||
| import '../../test_utils.dart'; | ||||
| import 'activity_mocks.dart'; | ||||
|  | ||||
| void main() { | ||||
|   late ActivityServiceMock activityMock; | ||||
|   late ProviderContainer container; | ||||
|   late ListenerMock<int> listener; | ||||
|  | ||||
|   setUp(() async { | ||||
|     activityMock = ActivityServiceMock(); | ||||
|     container = TestUtils.createContainer( | ||||
|       overrides: [ | ||||
|         activityServiceProvider.overrideWith((ref) => activityMock), | ||||
|       ], | ||||
|     ); | ||||
|     listener = ListenerMock(); | ||||
|   }); | ||||
|  | ||||
|   test('Returns the proper count family', () async { | ||||
|     when( | ||||
|       () => activityMock.getStatistics('test-album', assetId: 'test-asset'), | ||||
|     ).thenAnswer((_) async => 5); | ||||
|  | ||||
|     // Read here to make the getStatistics call | ||||
|     container.read(activityStatisticsProvider('test-album', 'test-asset')); | ||||
|  | ||||
|     container.listen( | ||||
|       activityStatisticsProvider('test-album', 'test-asset'), | ||||
|       listener, | ||||
|       fireImmediately: true, | ||||
|     ); | ||||
|  | ||||
|     // Sleep for the getStatistics future to resolve | ||||
|     await Future.delayed(const Duration(milliseconds: 1)); | ||||
|  | ||||
|     verifyInOrder([ | ||||
|       () => listener.call(null, 0), | ||||
|       () => listener.call(0, 5), | ||||
|     ]); | ||||
|  | ||||
|     verifyNoMoreInteractions(listener); | ||||
|   }); | ||||
|  | ||||
|   test('Adds activity', () async { | ||||
|     when( | ||||
|       () => activityMock.getStatistics('test-album'), | ||||
|     ).thenAnswer((_) async => 10); | ||||
|  | ||||
|     final provider = activityStatisticsProvider('test-album'); | ||||
|     container.listen( | ||||
|       provider, | ||||
|       listener, | ||||
|       fireImmediately: true, | ||||
|     ); | ||||
|  | ||||
|     // Sleep for the getStatistics future to resolve | ||||
|     await Future.delayed(const Duration(milliseconds: 1)); | ||||
|  | ||||
|     container.read(provider.notifier).addActivity(); | ||||
|     container.read(provider.notifier).addActivity(); | ||||
|  | ||||
|     expect(container.read(provider), 12); | ||||
|   }); | ||||
|  | ||||
|   test('Removes activity', () async { | ||||
|     when( | ||||
|       () => activityMock.getStatistics('new-album', assetId: 'test-asset'), | ||||
|     ).thenAnswer((_) async => 10); | ||||
|  | ||||
|     final provider = activityStatisticsProvider('new-album', 'test-asset'); | ||||
|     container.listen( | ||||
|       provider, | ||||
|       listener, | ||||
|       fireImmediately: true, | ||||
|     ); | ||||
|  | ||||
|     // Sleep for the getStatistics future to resolve | ||||
|     await Future.delayed(const Duration(milliseconds: 1)); | ||||
|  | ||||
|     container.read(provider.notifier).removeActivity(); | ||||
|     container.read(provider.notifier).removeActivity(); | ||||
|  | ||||
|     expect(container.read(provider), 8); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										199
									
								
								mobile/test/modules/activity/activity_text_field_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								mobile/test/modules/activity/activity_text_field_test.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| @Tags(['widget']) | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/activity_text_field.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/current_album.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/providers/user.provider.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| import '../../fixtures/album.stub.dart'; | ||||
| import '../../fixtures/user.stub.dart'; | ||||
| import '../../test_utils.dart'; | ||||
| import '../../widget_tester_extensions.dart'; | ||||
| import '../album/album_mocks.dart'; | ||||
| import '../shared/shared_mocks.dart'; | ||||
| import 'activity_mocks.dart'; | ||||
|  | ||||
| void main() { | ||||
|   late Isar db; | ||||
|   late MockCurrentAlbumProvider mockCurrentAlbumProvider; | ||||
|   late MockAlbumActivity activityMock; | ||||
|   late List<Override> overrides; | ||||
|  | ||||
|   setUpAll(() async { | ||||
|     TestUtils.init(); | ||||
|     db = await TestUtils.initIsar(); | ||||
|     Store.init(db); | ||||
|     Store.put(StoreKey.currentUser, UserStub.admin); | ||||
|     Store.put(StoreKey.serverEndpoint, ''); | ||||
|   }); | ||||
|  | ||||
|   setUp(() { | ||||
|     mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset); | ||||
|     activityMock = MockAlbumActivity(); | ||||
|     overrides = [ | ||||
|       currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider), | ||||
|       albumActivityProvider(AlbumStub.twoAsset.remoteId!) | ||||
|           .overrideWith(() => activityMock), | ||||
|     ]; | ||||
|   }); | ||||
|  | ||||
|   testWidgets('Returns an Input text field', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTextField( | ||||
|         onSubmit: (_) {}, | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     expect(find.byType(TextField), findsOneWidget); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('No UserCircleAvatar when user == null', (tester) async { | ||||
|     final userProvider = MockCurrentUserProvider(); | ||||
|  | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTextField( | ||||
|         onSubmit: (_) {}, | ||||
|       ), | ||||
|       overrides: [ | ||||
|         currentUserProvider.overrideWith((ref) => userProvider), | ||||
|         ...overrides, | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     expect(find.byType(UserCircleAvatar), findsNothing); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('UserCircleAvatar displayed when user != null', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTextField( | ||||
|         onSubmit: (_) {}, | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     expect(find.byType(UserCircleAvatar), findsOneWidget); | ||||
|   }); | ||||
|  | ||||
|   testWidgets( | ||||
|     'Filled icon if likedId != null', | ||||
|     (tester) async { | ||||
|       await tester.pumpConsumerWidget( | ||||
|         ActivityTextField( | ||||
|           onSubmit: (_) {}, | ||||
|           likeId: '1', | ||||
|         ), | ||||
|         overrides: overrides, | ||||
|       ); | ||||
|  | ||||
|       expect( | ||||
|         find.widgetWithIcon(IconButton, Icons.favorite_rounded), | ||||
|         findsOneWidget, | ||||
|       ); | ||||
|       expect( | ||||
|         find.widgetWithIcon(IconButton, Icons.favorite_border_rounded), | ||||
|         findsNothing, | ||||
|       ); | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   testWidgets('Bordered icon if likedId == null', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTextField( | ||||
|         onSubmit: (_) {}, | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     expect( | ||||
|       find.widgetWithIcon(IconButton, Icons.favorite_border_rounded), | ||||
|       findsOneWidget, | ||||
|     ); | ||||
|     expect( | ||||
|       find.widgetWithIcon(IconButton, Icons.favorite_rounded), | ||||
|       findsNothing, | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('Adds new like', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTextField( | ||||
|         onSubmit: (_) {}, | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     when(() => activityMock.addLike()).thenAnswer((_) => Future.value()); | ||||
|  | ||||
|     final suffixIcon = find.byType(IconButton); | ||||
|     await tester.tap(suffixIcon); | ||||
|  | ||||
|     verify(() => activityMock.addLike()); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('Removes like if already liked', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTextField( | ||||
|         onSubmit: (_) {}, | ||||
|         likeId: 'test-suffix', | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     when(() => activityMock.removeActivity(any())) | ||||
|         .thenAnswer((_) => Future.value()); | ||||
|  | ||||
|     final suffixIcon = find.byType(IconButton); | ||||
|     await tester.tap(suffixIcon); | ||||
|  | ||||
|     verify(() => activityMock.removeActivity('test-suffix')); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('Passes text entered to onSubmit on submit', (tester) async { | ||||
|     String? receivedText; | ||||
|  | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTextField( | ||||
|         onSubmit: (text) => receivedText = text, | ||||
|         likeId: 'test-suffix', | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     final textField = find.byType(TextField); | ||||
|     await tester.enterText(textField, 'This is a test comment'); | ||||
|     await tester.testTextInput.receiveAction(TextInputAction.done); | ||||
|     expect(receivedText, 'This is a test comment'); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('Input disabled when isEnabled false', (tester) async { | ||||
|     String? receviedText; | ||||
|  | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTextField( | ||||
|         onSubmit: (text) => receviedText = text, | ||||
|         isEnabled: false, | ||||
|         likeId: 'test-suffix', | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     final suffixIcon = find.byType(IconButton); | ||||
|     await tester.tap(suffixIcon, warnIfMissed: false); | ||||
|  | ||||
|     final textField = find.byType(TextField); | ||||
|     await tester.enterText(textField, 'This is a test comment'); | ||||
|     await tester.testTextInput.receiveAction(TextInputAction.done); | ||||
|  | ||||
|     expect(receviedText, isNull); | ||||
|     verifyNever(() => activityMock.addLike()); | ||||
|     verifyNever(() => activityMock.removeActivity(any())); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										222
									
								
								mobile/test/modules/activity/activity_tile_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								mobile/test/modules/activity/activity_tile_test.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,222 @@ | ||||
| @Tags(['widget']) | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/activities/models/activity.model.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
|  | ||||
| import '../../fixtures/asset.stub.dart'; | ||||
| import '../../fixtures/user.stub.dart'; | ||||
| import '../../test_utils.dart'; | ||||
| import '../../widget_tester_extensions.dart'; | ||||
| import '../asset_viewer/asset_viewer_mocks.dart'; | ||||
|  | ||||
| void main() { | ||||
|   late MockCurrentAssetProvider assetProvider; | ||||
|   late List<Override> overrides; | ||||
|   late Isar db; | ||||
|  | ||||
|   setUpAll(() async { | ||||
|     TestUtils.init(); | ||||
|     db = await TestUtils.initIsar(); | ||||
|     // For UserCircleAvatar | ||||
|     Store.init(db); | ||||
|     Store.put(StoreKey.currentUser, UserStub.admin); | ||||
|     Store.put(StoreKey.serverEndpoint, ''); | ||||
|     Store.put(StoreKey.accessToken, ''); | ||||
|   }); | ||||
|  | ||||
|   setUp(() { | ||||
|     assetProvider = MockCurrentAssetProvider(); | ||||
|     overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; | ||||
|   }); | ||||
|  | ||||
|   testWidgets('Returns a ListTile', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTile( | ||||
|         Activity( | ||||
|           id: '1', | ||||
|           createdAt: DateTime(100), | ||||
|           type: ActivityType.like, | ||||
|           user: UserStub.admin, | ||||
|         ), | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     expect(find.byType(ListTile), findsOneWidget); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('No trailing widget when activity assetId == null', | ||||
|       (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTile( | ||||
|         Activity( | ||||
|           id: '1', | ||||
|           createdAt: DateTime(100), | ||||
|           type: ActivityType.like, | ||||
|           user: UserStub.admin, | ||||
|         ), | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     final listTile = tester.widget<ListTile>(find.byType(ListTile)); | ||||
|     expect(listTile.trailing, isNull); | ||||
|   }); | ||||
|  | ||||
|   testWidgets( | ||||
|       'Asset Thumbanil as trailing widget when activity assetId != null', | ||||
|       (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTile( | ||||
|         Activity( | ||||
|           id: '1', | ||||
|           createdAt: DateTime(100), | ||||
|           type: ActivityType.like, | ||||
|           user: UserStub.admin, | ||||
|           assetId: '1', | ||||
|         ), | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     final listTile = tester.widget<ListTile>(find.byType(ListTile)); | ||||
|     expect(listTile.trailing, isNotNull); | ||||
|     // TODO: Validate this to be the common class after migrating ActivityTile#_ActivityAssetThumbnail to a common class | ||||
|   }); | ||||
|  | ||||
|   testWidgets('No trailing widget when current asset != null', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       ActivityTile( | ||||
|         Activity( | ||||
|           id: '1', | ||||
|           createdAt: DateTime(100), | ||||
|           type: ActivityType.like, | ||||
|           user: UserStub.admin, | ||||
|           assetId: '1', | ||||
|         ), | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     assetProvider.state = AssetStub.image1; | ||||
|     await tester.pumpAndSettle(); | ||||
|  | ||||
|     final listTile = tester.widget<ListTile>(find.byType(ListTile)); | ||||
|     expect(listTile.trailing, isNull); | ||||
|   }); | ||||
|  | ||||
|   group('Like Activity', () { | ||||
|     final activity = Activity( | ||||
|       id: '1', | ||||
|       createdAt: DateTime(100), | ||||
|       type: ActivityType.like, | ||||
|       user: UserStub.admin, | ||||
|     ); | ||||
|  | ||||
|     testWidgets('Like contains filled heart as leading', (tester) async { | ||||
|       await tester.pumpConsumerWidget( | ||||
|         ActivityTile(activity), | ||||
|         overrides: overrides, | ||||
|       ); | ||||
|  | ||||
|       // Leading widget should not be null | ||||
|       final listTile = tester.widget<ListTile>(find.byType(ListTile)); | ||||
|       expect(listTile.leading, isNotNull); | ||||
|  | ||||
|       // And should have a favorite icon | ||||
|       final favoIconFinder = find.widgetWithIcon( | ||||
|         listTile.leading!.runtimeType, | ||||
|         Icons.favorite_rounded, | ||||
|       ); | ||||
|  | ||||
|       expect(favoIconFinder, findsOneWidget); | ||||
|     }); | ||||
|  | ||||
|     testWidgets('Like title is center aligned', (tester) async { | ||||
|       await tester.pumpConsumerWidget( | ||||
|         ActivityTile(activity), | ||||
|         overrides: overrides, | ||||
|       ); | ||||
|  | ||||
|       final listTile = tester.widget<ListTile>(find.byType(ListTile)); | ||||
|  | ||||
|       expect(listTile.titleAlignment, ListTileTitleAlignment.center); | ||||
|     }); | ||||
|  | ||||
|     testWidgets('No subtitle for likes', (tester) async { | ||||
|       await tester.pumpConsumerWidget( | ||||
|         ActivityTile(activity), | ||||
|         overrides: overrides, | ||||
|       ); | ||||
|  | ||||
|       final listTile = tester.widget<ListTile>(find.byType(ListTile)); | ||||
|  | ||||
|       expect(listTile.subtitle, isNull); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   group('Comment Activity', () { | ||||
|     final activity = Activity( | ||||
|       id: '1', | ||||
|       createdAt: DateTime(100), | ||||
|       type: ActivityType.comment, | ||||
|       comment: 'This is a test comment', | ||||
|       user: UserStub.admin, | ||||
|     ); | ||||
|  | ||||
|     testWidgets('Comment contains User Circle Avatar as leading', | ||||
|         (tester) async { | ||||
|       await tester.pumpConsumerWidget( | ||||
|         ActivityTile(activity), | ||||
|         overrides: overrides, | ||||
|       ); | ||||
|  | ||||
|       final userAvatarFinder = find.byType(UserCircleAvatar); | ||||
|       expect(userAvatarFinder, findsOneWidget); | ||||
|  | ||||
|       // Leading widget should not be null | ||||
|       final listTile = tester.widget<ListTile>(find.byType(ListTile)); | ||||
|       expect(listTile.leading, isNotNull); | ||||
|  | ||||
|       // Make sure that the leading widget is the UserCircleAvatar | ||||
|       final userAvatar = tester.widget<UserCircleAvatar>(userAvatarFinder); | ||||
|       expect(listTile.leading, userAvatar); | ||||
|     }); | ||||
|  | ||||
|     testWidgets('Comment title is top aligned', (tester) async { | ||||
|       await tester.pumpConsumerWidget( | ||||
|         ActivityTile(activity), | ||||
|         overrides: overrides, | ||||
|       ); | ||||
|  | ||||
|       final listTile = tester.widget<ListTile>(find.byType(ListTile)); | ||||
|  | ||||
|       expect(listTile.titleAlignment, ListTileTitleAlignment.top); | ||||
|     }); | ||||
|  | ||||
|     testWidgets('Contains comment text as subtitle', (tester) async { | ||||
|       await tester.pumpConsumerWidget( | ||||
|         ActivityTile(activity), | ||||
|         overrides: overrides, | ||||
|       ); | ||||
|  | ||||
|       final listTile = tester.widget<ListTile>(find.byType(ListTile)); | ||||
|  | ||||
|       expect(listTile.subtitle, isNotNull); | ||||
|       expect( | ||||
|         find.descendant( | ||||
|           of: find.byType(ListTile), | ||||
|           matching: find.text(activity.comment!), | ||||
|         ), | ||||
|         findsOneWidget, | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										119
									
								
								mobile/test/modules/activity/dismissible_activity_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								mobile/test/modules/activity/dismissible_activity_test.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| @Tags(['widget']) | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| import 'package:immich_mobile/modules/activities/models/activity.model.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart'; | ||||
| import 'package:immich_mobile/modules/activities/widgets/dismissible_activity.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; | ||||
| import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| import '../../fixtures/user.stub.dart'; | ||||
| import '../../test_utils.dart'; | ||||
| import '../../widget_tester_extensions.dart'; | ||||
| import '../asset_viewer/asset_viewer_mocks.dart'; | ||||
|  | ||||
| final activity = Activity( | ||||
|   id: '1', | ||||
|   createdAt: DateTime(100), | ||||
|   type: ActivityType.like, | ||||
|   user: UserStub.admin, | ||||
| ); | ||||
|  | ||||
| void main() { | ||||
|   late MockCurrentAssetProvider assetProvider; | ||||
|   late List<Override> overrides; | ||||
|  | ||||
|   setUpAll(() => TestUtils.init()); | ||||
|  | ||||
|   setUp(() { | ||||
|     assetProvider = MockCurrentAssetProvider(); | ||||
|     overrides = [currentAssetProvider.overrideWith(() => assetProvider)]; | ||||
|   }); | ||||
|  | ||||
|   testWidgets('Returns a Dismissible', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       DismissibleActivity('1', ActivityTile(activity)), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     expect(find.byType(Dismissible), findsOneWidget); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('Dialog displayed when onDismiss is set', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     final dismissible = find.byType(Dismissible); | ||||
|     await tester.drag(dismissible, const Offset(500, 0)); | ||||
|     await tester.pumpAndSettle(); | ||||
|  | ||||
|     expect(find.byType(ConfirmDialog), findsOneWidget); | ||||
|   }); | ||||
|  | ||||
|   testWidgets( | ||||
|       'Ok action in ConfirmDialog should call onDismiss with activityId', | ||||
|       (tester) async { | ||||
|     String? receivedActivityId; | ||||
|     await tester.pumpConsumerWidget( | ||||
|       DismissibleActivity( | ||||
|         '1', | ||||
|         ActivityTile(activity), | ||||
|         onDismiss: (id) => receivedActivityId = id, | ||||
|       ), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     final dismissible = find.byType(Dismissible); | ||||
|     await tester.drag(dismissible, const Offset(-500, 0)); | ||||
|     await tester.pumpAndSettle(); | ||||
|  | ||||
|     final okButton = find.text('delete_dialog_ok'); | ||||
|     await tester.tap(okButton); | ||||
|     await tester.pumpAndSettle(); | ||||
|  | ||||
|     expect(receivedActivityId, '1'); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('Delete icon for background if onDismiss is set', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     final dismissible = find.byType(Dismissible); | ||||
|     await tester.drag(dismissible, const Offset(500, 0)); | ||||
|     await tester.pumpAndSettle(); | ||||
|  | ||||
|     expect(find.byIcon(Icons.delete_sweep_rounded), findsOneWidget); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('No delete dialog if onDismiss is not set', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       DismissibleActivity('1', ActivityTile(activity)), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     final dismissible = find.byType(Dismissible); | ||||
|     await tester.drag(dismissible, const Offset(500, 0)); | ||||
|     await tester.pumpAndSettle(); | ||||
|  | ||||
|     expect(find.byType(ConfirmDialog), findsNothing); | ||||
|   }); | ||||
|  | ||||
|   testWidgets('No icon for background if onDismiss is not set', (tester) async { | ||||
|     await tester.pumpConsumerWidget( | ||||
|       DismissibleActivity('1', ActivityTile(activity)), | ||||
|       overrides: overrides, | ||||
|     ); | ||||
|  | ||||
|     final dismissible = find.byType(Dismissible); | ||||
|     await tester.drag(dismissible, const Offset(-500, 0)); | ||||
|     await tester.pumpAndSettle(); | ||||
|  | ||||
|     expect(find.byIcon(Icons.delete_sweep_rounded), findsNothing); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										15
									
								
								mobile/test/modules/album/album_mocks.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								mobile/test/modules/album/album_mocks.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import 'package:immich_mobile/modules/album/providers/current_album.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
|  | ||||
| class MockCurrentAlbumProvider extends CurrentAlbum | ||||
|     with Mock | ||||
|     implements CurrentAlbumInternal { | ||||
|   Album? initAlbum; | ||||
|   MockCurrentAlbumProvider([this.initAlbum]); | ||||
|  | ||||
|   @override | ||||
|   Album? build() { | ||||
|     return initAlbum; | ||||
|   } | ||||
| } | ||||
| @@ -1,17 +1,17 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart'; | ||||
| import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; | ||||
| import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
| 
 | ||||
| import 'fixtures/album.stub.dart'; | ||||
| import 'fixtures/asset.stub.dart'; | ||||
| import 'mocks/app_settings_provider.mock.dart'; | ||||
| import 'test_utils.dart'; | ||||
| import '../../fixtures/album.stub.dart'; | ||||
| import '../../fixtures/asset.stub.dart'; | ||||
| import '../../test_utils.dart'; | ||||
| import '../settings/settings_mocks.dart'; | ||||
| 
 | ||||
| void main() { | ||||
|   /// Verify the sort modes | ||||
| @@ -48,15 +48,24 @@ void main() { | ||||
|       const created = AlbumSortMode.created; | ||||
|       test("Created time - ASC", () { | ||||
|         final sorted = created.sortFn(albums, false); | ||||
|         expect(sorted.isSortedBy((a) => a.createdAt), true); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.emptyAlbum, | ||||
|           AlbumStub.twoAsset, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.sharedWithUser, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
| 
 | ||||
|       test("Created time - DESC", () { | ||||
|         final sorted = created.sortFn(albums, true); | ||||
|         expect( | ||||
|           sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)), | ||||
|           true, | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.sharedWithUser, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.twoAsset, | ||||
|           AlbumStub.emptyAlbum, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
| @@ -64,18 +73,24 @@ void main() { | ||||
|       const assetCount = AlbumSortMode.assetCount; | ||||
|       test("Asset Count - ASC", () { | ||||
|         final sorted = assetCount.sortFn(albums, false); | ||||
|         expect( | ||||
|           sorted.isSorted((a, b) => a.assetCount.compareTo(b.assetCount)), | ||||
|           true, | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.emptyAlbum, | ||||
|           AlbumStub.sharedWithUser, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.twoAsset, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
| 
 | ||||
|       test("Asset Count - DESC", () { | ||||
|         final sorted = assetCount.sortFn(albums, true); | ||||
|         expect( | ||||
|           sorted.isSorted((b, a) => a.assetCount.compareTo(b.assetCount)), | ||||
|           true, | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.twoAsset, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.sharedWithUser, | ||||
|           AlbumStub.emptyAlbum, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
| @@ -83,18 +98,24 @@ void main() { | ||||
|       const lastModified = AlbumSortMode.lastModified; | ||||
|       test("Last modified - ASC", () { | ||||
|         final sorted = lastModified.sortFn(albums, false); | ||||
|         expect( | ||||
|           sorted.isSorted((a, b) => a.modifiedAt.compareTo(b.modifiedAt)), | ||||
|           true, | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.twoAsset, | ||||
|           AlbumStub.emptyAlbum, | ||||
|           AlbumStub.sharedWithUser, | ||||
|           AlbumStub.oneAsset, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
| 
 | ||||
|       test("Last modified - DESC", () { | ||||
|         final sorted = lastModified.sortFn(albums, true); | ||||
|         expect( | ||||
|           sorted.isSorted((b, a) => a.modifiedAt.compareTo(b.modifiedAt)), | ||||
|           true, | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.sharedWithUser, | ||||
|           AlbumStub.emptyAlbum, | ||||
|           AlbumStub.twoAsset, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
| @@ -102,18 +123,24 @@ void main() { | ||||
|       const created = AlbumSortMode.created; | ||||
|       test("Created - ASC", () { | ||||
|         final sorted = created.sortFn(albums, false); | ||||
|         expect( | ||||
|           sorted.isSorted((a, b) => a.createdAt.compareTo(b.createdAt)), | ||||
|           true, | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.emptyAlbum, | ||||
|           AlbumStub.twoAsset, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.sharedWithUser, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
| 
 | ||||
|       test("Created - DESC", () { | ||||
|         final sorted = created.sortFn(albums, true); | ||||
|         expect( | ||||
|           sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)), | ||||
|           true, | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.sharedWithUser, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.twoAsset, | ||||
|           AlbumStub.emptyAlbum, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
| @@ -122,28 +149,24 @@ void main() { | ||||
| 
 | ||||
|       test("Most Recent - ASC", () { | ||||
|         final sorted = mostRecent.sortFn(albums, false); | ||||
|         expect( | ||||
|           sorted, | ||||
|           [ | ||||
|             AlbumStub.sharedWithUser, | ||||
|             AlbumStub.twoAsset, | ||||
|             AlbumStub.oneAsset, | ||||
|             AlbumStub.emptyAlbum, | ||||
|           ], | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.sharedWithUser, | ||||
|           AlbumStub.twoAsset, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.emptyAlbum, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
| 
 | ||||
|       test("Most Recent - DESC", () { | ||||
|         final sorted = mostRecent.sortFn(albums, true); | ||||
|         expect( | ||||
|           sorted, | ||||
|           [ | ||||
|             AlbumStub.emptyAlbum, | ||||
|             AlbumStub.oneAsset, | ||||
|             AlbumStub.twoAsset, | ||||
|             AlbumStub.sharedWithUser, | ||||
|           ], | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.emptyAlbum, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.twoAsset, | ||||
|           AlbumStub.sharedWithUser, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
| @@ -152,28 +175,24 @@ void main() { | ||||
| 
 | ||||
|       test("Most Oldest - ASC", () { | ||||
|         final sorted = mostOldest.sortFn(albums, false); | ||||
|         expect( | ||||
|           sorted, | ||||
|           [ | ||||
|             AlbumStub.twoAsset, | ||||
|             AlbumStub.emptyAlbum, | ||||
|             AlbumStub.oneAsset, | ||||
|             AlbumStub.sharedWithUser, | ||||
|           ], | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.twoAsset, | ||||
|           AlbumStub.emptyAlbum, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.sharedWithUser, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
| 
 | ||||
|       test("Most Oldest - DESC", () { | ||||
|         final sorted = mostOldest.sortFn(albums, true); | ||||
|         expect( | ||||
|           sorted, | ||||
|           [ | ||||
|             AlbumStub.sharedWithUser, | ||||
|             AlbumStub.oneAsset, | ||||
|             AlbumStub.emptyAlbum, | ||||
|             AlbumStub.twoAsset, | ||||
|           ], | ||||
|         ); | ||||
|         final sortedList = [ | ||||
|           AlbumStub.sharedWithUser, | ||||
|           AlbumStub.oneAsset, | ||||
|           AlbumStub.emptyAlbum, | ||||
|           AlbumStub.twoAsset, | ||||
|         ]; | ||||
|         expect(sorted, orderedEquals(sortedList)); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| @@ -186,7 +205,9 @@ void main() { | ||||
|     setUp(() async { | ||||
|       settingsMock = AppSettingsServiceMock(); | ||||
|       container = TestUtils.createContainer( | ||||
|         overrides: [getAppSettingsServiceMock(settingsMock)], | ||||
|         overrides: [ | ||||
|           appSettingsServiceProvider.overrideWith((ref) => settingsMock), | ||||
|         ], | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
| @@ -196,7 +217,7 @@ void main() { | ||||
|         () => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder), | ||||
|       ).thenReturn(0); | ||||
| 
 | ||||
|       expect(AlbumSortMode.created, container.read(albumSortByOptionsProvider)); | ||||
|       expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created); | ||||
|     }); | ||||
| 
 | ||||
|     test('Returns the correct sort mode with index from Store', () { | ||||
| @@ -206,8 +227,8 @@ void main() { | ||||
|       ).thenReturn(3); | ||||
| 
 | ||||
|       expect( | ||||
|         AlbumSortMode.lastModified, | ||||
|         container.read(albumSortByOptionsProvider), | ||||
|         AlbumSortMode.lastModified, | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
| @@ -230,7 +251,6 @@ void main() { | ||||
|       ).thenReturn(0); | ||||
| 
 | ||||
|       final listener = ListenerMock<AlbumSortMode>(); | ||||
| 
 | ||||
|       container.listen( | ||||
|         albumSortByOptionsProvider, | ||||
|         listener, | ||||
| @@ -265,7 +285,9 @@ void main() { | ||||
|     setUp(() async { | ||||
|       settingsMock = AppSettingsServiceMock(); | ||||
|       container = TestUtils.createContainer( | ||||
|         overrides: [getAppSettingsServiceMock(settingsMock)], | ||||
|         overrides: [ | ||||
|           appSettingsServiceProvider.overrideWith((ref) => settingsMock), | ||||
|         ], | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
| @@ -274,7 +296,7 @@ void main() { | ||||
|         () => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse), | ||||
|       ).thenReturn(false); | ||||
| 
 | ||||
|       expect(false, container.read(albumSortOrderProvider)); | ||||
|       expect(container.read(albumSortOrderProvider), isFalse); | ||||
|     }); | ||||
| 
 | ||||
|     test('Properly saves the correct order', () { | ||||
| @@ -294,7 +316,6 @@ void main() { | ||||
|       ).thenReturn(false); | ||||
| 
 | ||||
|       final listener = ListenerMock<bool>(); | ||||
| 
 | ||||
|       container.listen( | ||||
|         albumSortOrderProvider, | ||||
|         listener, | ||||
							
								
								
									
										15
									
								
								mobile/test/modules/asset_viewer/asset_viewer_mocks.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								mobile/test/modules/asset_viewer/asset_viewer_mocks.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:mocktail/mocktail.dart'; | ||||
|  | ||||
| class MockCurrentAssetProvider extends CurrentAssetInternal | ||||
|     with Mock | ||||
|     implements CurrentAsset { | ||||
|   Asset? initAsset; | ||||
|   MockCurrentAssetProvider([this.initAsset]); | ||||
|  | ||||
|   @override | ||||
|   Asset? build() { | ||||
|     return initAsset; | ||||
|   } | ||||
| } | ||||
| @@ -49,8 +49,8 @@ void main() { | ||||
|       final a = makeAsset(id: '1', createdAt: createdAt); | ||||
|       final (dt, tz) = a.getTZAdjustedTimeAndOffset(); | ||||
| 
 | ||||
|       expect(dt, createdAt); | ||||
|       expect(tz, createdAt.timeZoneOffset); | ||||
|       expect(createdAt, dt); | ||||
|       expect(createdAt.timeZoneOffset, tz); | ||||
|     }); | ||||
| 
 | ||||
|     test('returns createdAt in local if in utc', () { | ||||
| @@ -59,8 +59,8 @@ void main() { | ||||
|       final (dt, tz) = a.getTZAdjustedTimeAndOffset(); | ||||
| 
 | ||||
|       final localCreatedAt = createdAt.toLocal(); | ||||
|       expect(dt, localCreatedAt); | ||||
|       expect(tz, localCreatedAt.timeZoneOffset); | ||||
|       expect(localCreatedAt, dt); | ||||
|       expect(localCreatedAt.timeZoneOffset, tz); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @@ -73,8 +73,8 @@ void main() { | ||||
|       final (dt, tz) = a.getTZAdjustedTimeAndOffset(); | ||||
| 
 | ||||
|       final dateTimeInUTC = dateTimeOriginal.toUtc(); | ||||
|       expect(dt, dateTimeInUTC); | ||||
|       expect(tz, dateTimeInUTC.timeZoneOffset); | ||||
|       expect(dateTimeInUTC, dt); | ||||
|       expect(dateTimeInUTC.timeZoneOffset, tz); | ||||
|     }); | ||||
| 
 | ||||
|     test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone', | ||||
| @@ -89,8 +89,8 @@ void main() { | ||||
|       final (dt, tz) = a.getTZAdjustedTimeAndOffset(); | ||||
| 
 | ||||
|       final dateTimeInUTC = dateTimeOriginal.toUtc(); | ||||
|       expect(dt, dateTimeInUTC); | ||||
|       expect(tz, dateTimeInUTC.timeZoneOffset); | ||||
|       expect(dateTimeInUTC, dt); | ||||
|       expect(dateTimeInUTC.timeZoneOffset, tz); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @@ -106,8 +106,8 @@ void main() { | ||||
| 
 | ||||
|       final adjustedTime = | ||||
|           TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location)); | ||||
|       expect(dt, adjustedTime); | ||||
|       expect(tz, adjustedTime.timeZoneOffset); | ||||
|       expect(adjustedTime, dt); | ||||
|       expect(adjustedTime.timeZoneOffset, tz); | ||||
|     }); | ||||
| 
 | ||||
|     test('With timezone as offset', () { | ||||
| @@ -124,8 +124,8 @@ void main() { | ||||
|       final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation); | ||||
| 
 | ||||
|       // Adds the offset to the actual time and returns the offset separately | ||||
|       expect(dt, adjustedTime); | ||||
|       expect(tz, offsetFromLocation); | ||||
|       expect(adjustedTime, dt); | ||||
|       expect(offsetFromLocation, tz); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| @@ -11,9 +11,9 @@ void main() { | ||||
|       ); | ||||
|     }); | ||||
|     test('malformed', () { | ||||
|       expect("".toDuration(), null); | ||||
|       expect("1:2".toDuration(), null); | ||||
|       expect("a:b:c".toDuration(), null); | ||||
|       expect("".toDuration(), isNull); | ||||
|       expect("1:2".toDuration(), isNull); | ||||
|       expect("a:b:c".toDuration(), isNull); | ||||
|     }); | ||||
|   }); | ||||
|   group('Test uniqueConsecutive', () { | ||||
| @@ -29,17 +29,17 @@ void main() { | ||||
| 
 | ||||
|     test('noDuplicates', () { | ||||
|       final a = [1, 2, 3]; | ||||
|       expect(a.uniqueConsecutive(), [1, 2, 3]); | ||||
|       expect(a.uniqueConsecutive(), orderedEquals([1, 2, 3])); | ||||
|     }); | ||||
| 
 | ||||
|     test('unsortedDuplicates', () { | ||||
|       final a = [1, 2, 1, 3]; | ||||
|       expect(a.uniqueConsecutive(), [1, 2, 1, 3]); | ||||
|       expect(a.uniqueConsecutive(), orderedEquals([1, 2, 1, 3])); | ||||
|     }); | ||||
| 
 | ||||
|     test('sortedDuplicates', () { | ||||
|       final a = [6, 6, 2, 3, 3, 3, 4, 5, 1, 1]; | ||||
|       expect(a.uniqueConsecutive(), [6, 2, 3, 4, 5, 1]); | ||||
|       expect(a.uniqueConsecutive(), orderedEquals([6, 2, 3, 4, 5, 1])); | ||||
|     }); | ||||
| 
 | ||||
|     test('withKey', () { | ||||
| @@ -48,7 +48,7 @@ void main() { | ||||
|         a.uniqueConsecutive( | ||||
|           compare: (s1, s2) => s1.length.compareTo(s2.length), | ||||
|         ), | ||||
|         ["a", "bb", "ddd"], | ||||
|         orderedEquals(["a", "bb", "ddd"]), | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user