mirror of
https://github.com/immich-app/immich.git
synced 2024-12-23 02:06:15 +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:
parent
d1e16025cf
commit
af32183728
@ -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 {}
|
||||
|
BIN
mobile/lib/modules/activities/providers/activity.provider.g.dart
generated
Normal file
BIN
mobile/lib/modules/activities/providers/activity.provider.g.dart
generated
Normal file
Binary file not shown.
@ -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));
|
BIN
mobile/lib/modules/activities/providers/activity_service.provider.g.dart
generated
Normal file
BIN
mobile/lib/modules/activities/providers/activity_service.provider.g.dart
generated
Normal file
Binary file not shown.
@ -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 {}
|
BIN
mobile/lib/modules/activities/providers/activity_statistics.provider.g.dart
generated
Normal file
BIN
mobile/lib/modules/activities/providers/activity_statistics.provider.g.dart
generated
Normal file
Binary file not shown.
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
Binary file not shown.
@ -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 {}
|
||||
|
BIN
mobile/lib/modules/album/providers/current_album.provider.g.dart
generated
Normal file
BIN
mobile/lib/modules/album/providers/current_album.provider.g.dart
generated
Normal file
Binary file not shown.
@ -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 {}
|
BIN
mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart
generated
Normal file
BIN
mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart
generated
Normal file
Binary file not shown.
@ -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
Loading…
Reference in New Issue
Block a user