more cleanup

This commit is contained in:
shenlong-tanwen
2026-07-28 00:51:05 +05:30
parent 8269c3422b
commit df6c362ffd
10 changed files with 48 additions and 57 deletions
+19 -24
View File
@@ -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<BaseAsset> assets, AssetFilter<RemoteAsset> ownedAssets});
final assetsActionProvider = Provider.family.autoDispose<AssetFilter<BaseAsset>, 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 <BaseAsset>{},
},
}),
);
class AssetsActionNotifier extends AutoDisposeFamilyNotifier<AssetsActionState, ActionSource> {
@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 <BaseAsset>{},
},
};
final assets = AssetFilter(selected);
return (assets: assets, ownedAssets: assets.owned(ref.watch(authUserProvider).id));
final clearSelectionProvider = Provider.family.autoDispose<VoidCallback, ActionSource>((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, AssetsActionState, ActionSource>(
AssetsActionNotifier.new,
final ownedAssetsActionProvider = Provider.family.autoDispose<AssetFilter<RemoteAsset>, ActionSource>(
(ref, source) => ref.watch(assetsActionProvider(source)).owned(ref.watch(authUserProvider).id),
);
abstract class AssetActionBuilder extends ActionBuilder {
@@ -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);
}
}
@@ -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) {
@@ -10,13 +10,13 @@ import 'package:immich_mobile/utils/error_handler.dart';
typedef _State = ({bool shouldFavorite, List<String> 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");
}
@@ -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<void> _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<void> _remove(BuildContext context, WidgetRef ref) async {
@@ -67,14 +67,9 @@ class PartnerRemoveAction extends ActionBuilder {
}
@visibleForTesting
final candidatesStateProvider = StreamProvider.autoDispose<Iterable<User>>((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<Iterable<User>>(
(ref) => ref.watch(partnerServiceProvider).getCandidates(ref.watch(authUserProvider).id),
);
@visibleForTesting
class PartnerSelectionDialog extends ConsumerWidget {
@@ -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<ToastRepository>((ref) => const .new());
final toastServiceProvider = Provider<ToastService>((ref) => const .new());
@@ -9,8 +9,8 @@ class ToastOption {
const ToastOption({this.timeout, this.onUndo});
}
class ToastRepository {
const ToastRepository();
class ToastService {
const ToastService();
FutureOr<void> success(String message, {ToastOption? toast}) {
snackbar.success(message, duration: toast?.timeout);
-3
View File
@@ -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 {}
+3
View File
@@ -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 {}
+2 -2
View File
@@ -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();