diff --git a/mobile/lib/presentation/actions/action.dart b/mobile/lib/presentation/actions/action.dart index b0f0f7a370..4a26e565c5 100644 --- a/mobile/lib/presentation/actions/action.dart +++ b/mobile/lib/presentation/actions/action.dart @@ -1,4 +1,4 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -19,35 +19,30 @@ class ActionItem { abstract class ActionBuilder { const ActionBuilder(); - ActionItem? build(BuildContext context, WidgetRef ref); + // null when the action is not applicable for the current context + ActionItem? create(BuildContext context, WidgetRef ref); } -typedef AssetsActionState = ({AssetFilter assets, AssetFilter ownedAssets}); +final assetsActionProvider = Provider.family.autoDispose, ActionSource>( + (ref, source) => AssetFilter(switch (source) { + .timeline => ref.watch(multiSelectProvider.select((s) => s.selectedAssets)), + .viewer => switch (ref.watch(assetViewerProvider.select((s) => s.currentAsset))) { + BaseAsset asset => {asset}, + null => const {}, + }, + }), +); -class AssetsActionNotifier extends AutoDisposeFamilyNotifier { - @override - AssetsActionState build(ActionSource source) { - final selected = switch (source) { - .timeline => ref.watch(multiSelectProvider.select((s) => s.selectedAssets)), - .viewer => switch (ref.watch(assetViewerProvider.select((s) => s.currentAsset))) { - BaseAsset asset => {asset}, - null => const {}, - }, - }; - - final assets = AssetFilter(selected); - return (assets: assets, ownedAssets: assets.owned(ref.watch(authUserProvider).id)); +final clearSelectionProvider = Provider.family.autoDispose((ref, source) { + if (source == .timeline) { + return ref.read(multiSelectProvider.notifier).reset; } - void clearSelect() { - if (arg == .timeline) { - ref.read(multiSelectProvider.notifier).reset(); - } - } -} + return () {}; +}); -final assetsActionProvider = NotifierProvider.family.autoDispose( - AssetsActionNotifier.new, +final ownedAssetsActionProvider = Provider.family.autoDispose, ActionSource>( + (ref, source) => ref.watch(assetsActionProvider(source)).owned(ref.watch(authUserProvider).id), ); abstract class AssetActionBuilder extends ActionBuilder { diff --git a/mobile/lib/presentation/actions/action.widget.dart b/mobile/lib/presentation/actions/action.widget.dart index 26467efe1f..f930b3230d 100644 --- a/mobile/lib/presentation/actions/action.widget.dart +++ b/mobile/lib/presentation/actions/action.widget.dart @@ -12,12 +12,12 @@ abstract class ActionWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final resolved = action.build(context, ref); - if (resolved == null) { + final actionItem = action.create(context, ref); + if (actionItem == null) { return const SizedBox.shrink(); } - return builder(context, ref, resolved); + return builder(context, ref, actionItem); } } diff --git a/mobile/lib/presentation/actions/asset_debug.action.dart b/mobile/lib/presentation/actions/asset_debug.action.dart index b201b93ba9..3294f02d5f 100644 --- a/mobile/lib/presentation/actions/asset_debug.action.dart +++ b/mobile/lib/presentation/actions/asset_debug.action.dart @@ -12,7 +12,7 @@ class AssetDebugAction extends AssetActionBuilder { const AssetDebugAction({required super.source}); @override - ActionItem? build(BuildContext context, WidgetRef ref) { + ActionItem? create(BuildContext context, WidgetRef ref) { final assets = ref.watch(assetsActionProvider(source)).assets; final troubleshootEnabled = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); if (!troubleshootEnabled || assets.length != 1) { diff --git a/mobile/lib/presentation/actions/favorite.action.dart b/mobile/lib/presentation/actions/favorite.action.dart index e93c1ef8d1..9ab3770163 100644 --- a/mobile/lib/presentation/actions/favorite.action.dart +++ b/mobile/lib/presentation/actions/favorite.action.dart @@ -10,13 +10,13 @@ import 'package:immich_mobile/utils/error_handler.dart'; typedef _State = ({bool shouldFavorite, List assetIds}); final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { - final AssetsActionState(:ownedAssets) = ref.watch(assetsActionProvider(source)); - if (ownedAssets.isEmpty) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + if (assets.isEmpty) { return null; } - final shouldFavorite = ownedAssets.favorite(isFavorite: false).isNotEmpty; - final assetIds = ownedAssets.favorite(isFavorite: !shouldFavorite).map((asset) => asset.id).toList(growable: false); + final shouldFavorite = assets.favorite(isFavorite: false).isNotEmpty; + final assetIds = assets.favorite(isFavorite: !shouldFavorite).map((asset) => asset.id).toList(growable: false); return (shouldFavorite: shouldFavorite, assetIds: assetIds); }); @@ -24,7 +24,7 @@ class FavoriteAction extends AssetActionBuilder { const FavoriteAction({required super.source}); @override - ActionItem? build(BuildContext context, WidgetRef ref) { + ActionItem? create(BuildContext context, WidgetRef ref) { final shouldFavorite = ref.watch(_stateProvider(source).select((state) => state?.shouldFavorite)); if (shouldFavorite == null) { return null; @@ -47,13 +47,14 @@ class FavoriteAction extends AssetActionBuilder { final message = shouldFavorite ? context.t.favorite_action_prompt(count: assetIds.length) : context.t.unfavorite_action_prompt(count: assetIds.length); - final toast = ref.read(toastRepositoryProvider); - final selection = ref.read(assetsActionProvider(source).notifier); + final assertService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); try { - await ref.read(assetServiceProvider).update(assetIds, isFavorite: .some(shouldFavorite)); - toast.success(message); - selection.clearSelect(); + await assertService.update(assetIds, isFavorite: .some(shouldFavorite)); + toastService.success(message); + clearSelection(); } catch (error, stack) { handleError(error, stack: stack, description: "Failed to update favorite status for assets"); } diff --git a/mobile/lib/presentation/actions/partner.action.dart b/mobile/lib/presentation/actions/partner.action.dart index c246e08d75..012340c1d8 100644 --- a/mobile/lib/presentation/actions/partner.action.dart +++ b/mobile/lib/presentation/actions/partner.action.dart @@ -13,7 +13,7 @@ class PartnerAddAction extends ActionBuilder { const PartnerAddAction(); @override - ActionItem build(BuildContext context, WidgetRef ref) => + ActionItem create(BuildContext context, WidgetRef ref) => ActionItem(icon: Icons.person_add_rounded, label: context.t.add_partner, onAction: () => _add(context, ref)); Future _add(BuildContext context, WidgetRef ref) async { @@ -40,7 +40,7 @@ class PartnerRemoveAction extends ActionBuilder { final String partnerName; @override - ActionItem build(BuildContext context, WidgetRef ref) => + ActionItem create(BuildContext context, WidgetRef ref) => ActionItem(icon: Icons.person_remove_rounded, label: context.t.remove, onAction: () => _remove(context, ref)); Future _remove(BuildContext context, WidgetRef ref) async { @@ -67,14 +67,9 @@ class PartnerRemoveAction extends ActionBuilder { } @visibleForTesting -final candidatesStateProvider = StreamProvider.autoDispose>((ref) { - final currentUser = ref.watch(currentUserProvider); - // TODO: Refactor with a route guard to avoid this check in every provider - if (currentUser == null) { - return const Stream.empty(); - } - return ref.watch(partnerServiceProvider).getCandidates(currentUser.id); -}); +final candidatesStateProvider = StreamProvider.autoDispose>( + (ref) => ref.watch(partnerServiceProvider).getCandidates(ref.watch(authUserProvider).id), +); @visibleForTesting class PartnerSelectionDialog extends ConsumerWidget { diff --git a/mobile/lib/providers/infrastructure/toast.provider.dart b/mobile/lib/providers/infrastructure/toast.provider.dart index 27d1cf9e6b..eaaffd6fca 100644 --- a/mobile/lib/providers/infrastructure/toast.provider.dart +++ b/mobile/lib/providers/infrastructure/toast.provider.dart @@ -1,4 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/repositories/toast.repository.dart'; +import 'package:immich_mobile/services/toast.service.dart'; -final toastRepositoryProvider = Provider((ref) => const .new()); +final toastServiceProvider = Provider((ref) => const .new()); diff --git a/mobile/lib/repositories/toast.repository.dart b/mobile/lib/services/toast.service.dart similarity index 91% rename from mobile/lib/repositories/toast.repository.dart rename to mobile/lib/services/toast.service.dart index 0cca50fdec..2b61a945ff 100644 --- a/mobile/lib/repositories/toast.repository.dart +++ b/mobile/lib/services/toast.service.dart @@ -9,8 +9,8 @@ class ToastOption { const ToastOption({this.timeout, this.onUndo}); } -class ToastRepository { - const ToastRepository(); +class ToastService { + const ToastService(); FutureOr success(String message, {ToastOption? toast}) { snackbar.success(message, duration: toast?.timeout); diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 82c9395b58..b56a8a098a 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -6,7 +6,6 @@ import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/permission.repository.dart'; -import 'package:immich_mobile/repositories/toast.repository.dart'; import 'package:mocktail/mocktail.dart'; class MockAssetApiRepository extends Mock implements AssetApiRepository {} @@ -24,5 +23,3 @@ class MockTagService extends Mock implements TagService {} class MockDownloadRepository extends Mock implements DownloadRepository {} class MockRemoteExifRepository extends Mock implements RemoteExifRepository {} - -class MockToastRepository extends Mock implements ToastRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 300c54dcbb..785567de56 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/services/gcast.service.dart'; import 'package:immich_mobile/services/network.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; +import 'package:immich_mobile/services/toast.service.dart'; import 'package:mocktail/mocktail.dart'; class MockApiService extends Mock implements ApiService {} @@ -43,3 +44,5 @@ class MockServerInfoService extends Mock implements ServerInfoService {} class MockCleanupService extends Mock implements CleanupService {} class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} + +class MockToastService extends Mock implements ToastService {} diff --git a/mobile/test/unit/mocks.dart b/mobile/test/unit/mocks.dart index d8eadda7ae..7dd15eb9a3 100644 --- a/mobile/test/unit/mocks.dart +++ b/mobile/test/unit/mocks.dart @@ -30,7 +30,6 @@ class RepositoryMocks { final remoteAsset = RemoteAssetRepositoryStub(MockRemoteAssetRepository()); final remoteExif = RemoteExifRepositoryStub(MockRemoteExifRepository()); final trashedAsset = MockTrashedLocalAssetRepository(); - final toast = MockToastRepository(); final remoteAlbum = MockRemoteAlbumRepository(); final albumApi = MockDriftAlbumApiRepository(); @@ -56,7 +55,6 @@ class RepositoryMocks { assetApi.reset(); assetMedia.reset(); download.reset(); - reset(toast); _stubLocalAlbumRepository(); _stubLocalAssetRepository(); _stubRemoteAssetRepository(); @@ -115,6 +113,7 @@ class ServiceMocks { final upload = MockForegroundUploadService(); final cast = MockGCastService(); final serverInfo = MockServerInfoService(); + final toast = MockToastService(); ServiceMocks() { resetAll(); @@ -132,6 +131,7 @@ class ServiceMocks { reset(serverInfo); reset(backgroundSync); reset(upload); + reset(toast); _stubUserService(); _stubPartnerService(); _stubAssetService();