From 21f500191a9b5413a12feffd9cb0bd3416d64f4a Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 1 Jul 2025 08:10:25 +0530 Subject: [PATCH] refactor: actions provider (#19651) * refactor: actions provider * chore: rename error and stack * remove empty checks --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../lib/domain/services/timeline.service.dart | 4 +- .../archive_action_button.widget.dart | 42 ++----- .../favorite_action_button.widget.dart | 42 ++----- .../widgets/images/thumbnail_tile.widget.dart | 7 +- .../widgets/timeline/timeline.widget.dart | 2 +- .../infrastructure/action.provider.dart | 108 ++++++++++++++++-- .../timeline/multiselect.provider.dart | 37 ++---- mobile/lib/services/action.service.dart | 57 ++++----- 8 files changed, 159 insertions(+), 140 deletions(-) diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 1dd2dfa150..56a12cac07 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -64,7 +64,7 @@ class TimelineService { }) : _assetSource = assetSource, _bucketSource = bucketSource { _bucketSubscription = - _bucketSource().listen((_) => unawaited(_reloadBucket())); + _bucketSource().listen((_) => unawaited(reloadBucket())); } final AsyncMutex _mutex = AsyncMutex(); @@ -74,7 +74,7 @@ class TimelineService { Stream> Function() get watchBuckets => _bucketSource; - Future _reloadBucket() => _mutex.run(() async { + Future reloadBucket() => _mutex.run(() async { _buffer = await _assetSource(_bufferOffset, _buffer.length); }); diff --git a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart index 046ee051fc..5ec88231ca 100644 --- a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class ArchiveActionButton extends ConsumerWidget { @@ -15,45 +14,28 @@ class ArchiveActionButton extends ConsumerWidget { const ArchiveActionButton({super.key, required this.source}); - onAction(BuildContext context, WidgetRef ref) { - switch (source) { - case ActionSource.timeline: - timelineAction(context, ref); - case ActionSource.viewer: - viewerAction(ref); - } - } - - void timelineAction(BuildContext context, WidgetRef ref) { - final user = ref.read(currentUserProvider); - if (user == null) { + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { return; } - final ids = ref - .read(multiSelectProvider.select((value) => value.selectedAssets)) - .whereType() - .where((asset) => asset.ownerId == user.id) - .map((asset) => asset.id) - .toList(); - - if (ids.isEmpty) { - return; - } - - ref.read(actionProvider.notifier).archive(ids); + final result = await ref.read(actionProvider.notifier).archive(source); + await ref.read(timelineServiceProvider).reloadBucket(); ref.read(multiSelectProvider.notifier).reset(); - final toastMessage = 'archive_action_prompt'.t( + final successMessage = 'archive_action_prompt'.t( context: context, - args: {'count': ids.length.toString()}, + args: {'count': result.count.toString()}, ); if (context.mounted) { ImmichToast.show( context: context, - msg: toastMessage, + msg: result.success + ? successMessage + : 'scaffold_body_error_occurred'.t(context: context), gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, ); } } @@ -67,7 +49,7 @@ class ArchiveActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.archive_outlined, label: "archive".t(context: context), - onPressed: () => onAction(context, ref), + onPressed: () => _onTap(context, ref), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart index 5bf0566f20..dbe43d2f17 100644 --- a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class FavoriteActionButton extends ConsumerWidget { @@ -15,45 +14,28 @@ class FavoriteActionButton extends ConsumerWidget { const FavoriteActionButton({super.key, required this.source}); - onAction(BuildContext context, WidgetRef ref) { - switch (source) { - case ActionSource.timeline: - timelineAction(context, ref); - case ActionSource.viewer: - viewerAction(ref); - } - } - - void timelineAction(BuildContext context, WidgetRef ref) { - final user = ref.read(currentUserProvider); - if (user == null) { + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { return; } - final ids = ref - .read(multiSelectProvider.select((value) => value.selectedAssets)) - .whereType() - .where((asset) => asset.ownerId == user.id) - .map((asset) => asset.id) - .toList(); - - if (ids.isEmpty) { - return; - } - - ref.read(actionProvider.notifier).favorite(ids); + final result = await ref.read(actionProvider.notifier).favorite(source); + await ref.read(timelineServiceProvider).reloadBucket(); ref.read(multiSelectProvider.notifier).reset(); - final toastMessage = 'favorite_action_prompt'.t( + final successMessage = 'favorite_action_prompt'.t( context: context, - args: {'count': ids.length.toString()}, + args: {'count': result.count.toString()}, ); if (context.mounted) { ImmichToast.show( context: context, - msg: toastMessage, + msg: result.success + ? successMessage + : 'scaffold_body_error_occurred'.t(context: context), gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, ); } } @@ -67,7 +49,7 @@ class FavoriteActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.favorite_border_rounded, label: "favorite".t(context: context), - onPressed: () => onAction(context, ref), + onPressed: () => _onTap(context, ref), ); } } diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index ba02ea56e8..f243fb1130 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -30,8 +30,11 @@ class ThumbnailTile extends ConsumerWidget { ? context.primaryColor.darken(amount: 0.6) : context.primaryColor.lighten(amount: 0.8); - final multiselect = ref.watch(multiSelectProvider); - final isSelected = multiselect.selectedAssets.contains(asset); + final isSelected = ref.watch( + multiSelectProvider.select( + (multiselect) => multiselect.selectedAssets.contains(asset), + ), + ); return Stack( children: [ diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 688675c686..fd0806cff0 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -407,7 +407,7 @@ class _MultiSelectStatusButton extends ConsumerWidget { final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length)); return ElevatedButton.icon( - onPressed: () => ref.read(multiSelectProvider.notifier).clearSelection(), + onPressed: () => ref.read(multiSelectProvider.notifier).reset(), icon: Icon( Icons.close_rounded, color: context.colorScheme.onPrimary, diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index f5fb83bd5a..0b7fcd1469 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -1,14 +1,36 @@ +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/multiselect.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/action.service.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; final actionProvider = NotifierProvider( ActionNotifier.new, dependencies: [ actionServiceProvider, + timelineServiceProvider, + multiselectProvider, ], ); +class ActionResult { + final int count; + final bool success; + final String? error; + + const ActionResult({required this.count, required this.success, this.error}); + + @override + String toString() => + 'ActionResult(count: $count, success: $success, error: $error)'; +} + class ActionNotifier extends Notifier { + final Logger _logger = Logger('ActionNotifier'); late final ActionService _service; ActionNotifier() : super(); @@ -18,19 +40,89 @@ class ActionNotifier extends Notifier { _service = ref.watch(actionServiceProvider); } - Future favorite(List ids) async { - await _service.favorite(ids); + List _getIdsForSource(ActionSource source) { + final currentUser = ref.read(currentUserProvider); + if (T is RemoteAsset && currentUser == null) { + return []; + } + + final Set assets = switch (source) { + ActionSource.timeline => + ref.read(multiSelectProvider.select((s) => s.selectedAssets)), + ActionSource.viewer => {}, + }; + + return switch (T) { + const (RemoteAsset) => assets + .where( + (asset) => asset is RemoteAsset && asset.ownerId == currentUser!.id, + ) + .cast() + .map((asset) => asset.id) + .toList(), + const (LocalAsset) => + assets.whereType().map((asset) => asset.id).toList(), + _ => [], + }; } - Future unFavorite(List ids) async { - await _service.unFavorite(ids); + Future favorite(ActionSource source) async { + final ids = _getIdsForSource(source); + try { + await _service.favorite(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to favorite assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } } - Future archive(List ids) async { - await _service.archive(ids); + Future unFavorite(ActionSource source) async { + final ids = _getIdsForSource(source); + try { + await _service.unFavorite(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to unfavorite assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } } - Future unArchive(List ids) async { - await _service.unArchive(ids); + Future archive(ActionSource source) async { + final ids = _getIdsForSource(source); + try { + await _service.archive(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to archive assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } + + Future unArchive(ActionSource source) async { + final ids = _getIdsForSource(source); + try { + await _service.unArchive(ids); + return ActionResult(count: ids.length, success: true); + } catch (error, stack) { + _logger.severe('Failed to unarchive assets', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } } } diff --git a/mobile/lib/providers/timeline/multiselect.provider.dart b/mobile/lib/providers/timeline/multiselect.provider.dart index 1bd0ae1565..2f2d76c68c 100644 --- a/mobile/lib/providers/timeline/multiselect.provider.dart +++ b/mobile/lib/providers/timeline/multiselect.provider.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; - import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -13,12 +12,8 @@ final multiSelectProvider = class MultiSelectState { final Set selectedAssets; - final int lastUpdatedTime; - const MultiSelectState({ - required this.selectedAssets, - required this.lastUpdatedTime, - }); + const MultiSelectState({required this.selectedAssets}); bool get isEnabled => selectedAssets.isNotEmpty; bool get hasRemote => selectedAssets.any( @@ -30,31 +25,25 @@ class MultiSelectState { (asset) => asset.storage == AssetState.local, ); - MultiSelectState copyWith({ - Set? selectedAssets, - int? lastUpdatedTime, - }) { + MultiSelectState copyWith({Set? selectedAssets}) { return MultiSelectState( selectedAssets: selectedAssets ?? this.selectedAssets, - lastUpdatedTime: lastUpdatedTime ?? this.lastUpdatedTime, ); } @override - String toString() => - 'MultiSelectState(selectedAssets: $selectedAssets, lastUpdatedTime: $lastUpdatedTime)'; + String toString() => 'MultiSelectState(selectedAssets: $selectedAssets)'; @override bool operator ==(covariant MultiSelectState other) { if (identical(this, other)) return true; final listEquals = const DeepCollectionEquality().equals; - return listEquals(other.selectedAssets, selectedAssets) && - other.lastUpdatedTime == lastUpdatedTime; + return listEquals(other.selectedAssets, selectedAssets); } @override - int get hashCode => selectedAssets.hashCode ^ lastUpdatedTime.hashCode; + int get hashCode => selectedAssets.hashCode; } class MultiSelectNotifier extends Notifier { @@ -64,10 +53,7 @@ class MultiSelectNotifier extends Notifier { MultiSelectState build() { _timelineService = ref.read(timelineServiceProvider); - return const MultiSelectState( - selectedAssets: {}, - lastUpdatedTime: 0, - ); + return const MultiSelectState(selectedAssets: {}); } void selectAsset(BaseAsset asset) { @@ -98,17 +84,8 @@ class MultiSelectNotifier extends Notifier { } } - void clearSelection() { - state = state.copyWith( - selectedAssets: {}, - ); - } - void reset() { - state = MultiSelectState( - selectedAssets: {}, - lastUpdatedTime: DateTime.now().millisecondsSinceEpoch, - ); + state = const MultiSelectState(selectedAssets: {}); } /// Bucket bulk operations diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 29afb3a331..afc4e3f776 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; @@ -19,50 +18,34 @@ class ActionService { const ActionService(this._assetApiRepository, this._remoteAssetRepository); Future favorite(List remoteIds) async { - try { - await _assetApiRepository.updateFavorite(remoteIds, true); - await _remoteAssetRepository.updateFavorite(remoteIds, true); - } catch (e) { - debugPrint('Error favoriting assets: $e'); - } + await _assetApiRepository.updateFavorite(remoteIds, true); + await _remoteAssetRepository.updateFavorite(remoteIds, true); } Future unFavorite(List remoteIds) async { - try { - await _assetApiRepository.updateFavorite(remoteIds, false); - await _remoteAssetRepository.updateFavorite(remoteIds, false); - } catch (e) { - debugPrint('Error unfavoriting assets: $e'); - } + await _assetApiRepository.updateFavorite(remoteIds, false); + await _remoteAssetRepository.updateFavorite(remoteIds, false); } Future archive(List remoteIds) async { - try { - await _assetApiRepository.updateVisibility( - remoteIds, - AssetVisibilityEnum.archive, - ); - await _remoteAssetRepository.updateVisibility( - remoteIds, - AssetVisibility.archive, - ); - } catch (e) { - debugPrint('Error archive assets: $e'); - } + await _assetApiRepository.updateVisibility( + remoteIds, + AssetVisibilityEnum.archive, + ); + await _remoteAssetRepository.updateVisibility( + remoteIds, + AssetVisibility.archive, + ); } Future unArchive(List remoteIds) async { - try { - await _assetApiRepository.updateVisibility( - remoteIds, - AssetVisibilityEnum.timeline, - ); - await _remoteAssetRepository.updateVisibility( - remoteIds, - AssetVisibility.timeline, - ); - } catch (e) { - debugPrint('Error unarchive assets: $e'); - } + await _assetApiRepository.updateVisibility( + remoteIds, + AssetVisibilityEnum.timeline, + ); + await _remoteAssetRepository.updateVisibility( + remoteIds, + AssetVisibility.timeline, + ); } }