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>
This commit is contained in:
shenlong 2025-07-01 08:10:25 +05:30 committed by GitHub
parent 5011636d95
commit 21f500191a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 159 additions and 140 deletions

View File

@ -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<List<Bucket>> Function() get watchBuckets => _bucketSource;
Future<void> _reloadBucket() => _mutex.run(() async {
Future<void> reloadBucket() => _mutex.run(() async {
_buffer = await _assetSource(_bufferOffset, _buffer.length);
});

View File

@ -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<RemoteAsset>()
.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),
);
}
}

View File

@ -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<RemoteAsset>()
.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),
);
}
}

View File

@ -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: [

View File

@ -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,

View File

@ -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, void>(
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<void> {
final Logger _logger = Logger('ActionNotifier');
late final ActionService _service;
ActionNotifier() : super();
@ -18,19 +40,89 @@ class ActionNotifier extends Notifier<void> {
_service = ref.watch(actionServiceProvider);
}
Future<void> favorite(List<String> ids) async {
await _service.favorite(ids);
List<String> _getIdsForSource<T extends BaseAsset>(ActionSource source) {
final currentUser = ref.read(currentUserProvider);
if (T is RemoteAsset && currentUser == null) {
return [];
}
final Set<BaseAsset> 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<RemoteAsset>()
.map((asset) => asset.id)
.toList(),
const (LocalAsset) =>
assets.whereType<LocalAsset>().map((asset) => asset.id).toList(),
_ => [],
};
}
Future<void> unFavorite(List<String> ids) async {
await _service.unFavorite(ids);
Future<ActionResult> favorite(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(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<void> archive(List<String> ids) async {
await _service.archive(ids);
Future<ActionResult> unFavorite(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(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<void> unArchive(List<String> ids) async {
await _service.unArchive(ids);
Future<ActionResult> archive(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(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<ActionResult> unArchive(ActionSource source) async {
final ids = _getIdsForSource<RemoteAsset>(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(),
);
}
}
}

View File

@ -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<BaseAsset> 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<BaseAsset>? selectedAssets,
int? lastUpdatedTime,
}) {
MultiSelectState copyWith({Set<BaseAsset>? 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<MultiSelectState> {
@ -64,10 +53,7 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
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<MultiSelectState> {
}
}
void clearSelection() {
state = state.copyWith(
selectedAssets: {},
);
}
void reset() {
state = MultiSelectState(
selectedAssets: {},
lastUpdatedTime: DateTime.now().millisecondsSinceEpoch,
);
state = const MultiSelectState(selectedAssets: {});
}
/// Bucket bulk operations

View File

@ -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<void> favorite(List<String> 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<void> unFavorite(List<String> 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<void> archive(List<String> 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<void> unArchive(List<String> 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,
);
}
}