mirror of
https://github.com/immich-app/immich.git
synced 2026-05-26 17:02:36 -04:00
feat(mobile): "Add Tags" asset multiselect option (#26269)
* add bulk_tag_assets_action_button to general_bottom_sheet.widget include create tag tile in 'Add Tags' action modal * follow provider -> svc -> repo pattern for tags * rebase and cleanup --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
@@ -823,6 +823,7 @@
|
||||
"contain": "Contain",
|
||||
"context": "Context",
|
||||
"continue": "Continue",
|
||||
"control_bottom_app_bar_add_tags": "Add Tags",
|
||||
"control_bottom_app_bar_create_new_album": "Create new album",
|
||||
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
|
||||
"control_bottom_app_bar_delete_from_local": "Delete from device",
|
||||
@@ -1074,6 +1075,7 @@
|
||||
"failed_to_remove_product_key": "Failed to remove product key",
|
||||
"failed_to_reset_pin_code": "Failed to reset PIN code",
|
||||
"failed_to_stack_assets": "Failed to stack assets",
|
||||
"failed_to_tag_assets": "Failed to tag assets",
|
||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||
"failed_to_update_notification_status": "Failed to update notification status",
|
||||
"incorrect_email_or_password": "Incorrect email or password",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/tags_api.repository.dart';
|
||||
|
||||
final tagServiceProvider = Provider<TagService>((ref) => TagService(ref.watch(tagsApiRepositoryProvider)));
|
||||
|
||||
class TagService {
|
||||
final TagsApiRepository _repository;
|
||||
|
||||
const TagService(this._repository);
|
||||
|
||||
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||
return _repository.bulkTagAssets(assetIds, tagIds);
|
||||
}
|
||||
|
||||
Future<Set<Tag>> getAllTags() async {
|
||||
final dtos = await _repository.getAllTags();
|
||||
if (dtos == null) {
|
||||
return {};
|
||||
}
|
||||
return dtos.map((dto) => Tag.fromDto(dto)).toSet();
|
||||
}
|
||||
|
||||
Future<List<Tag>> upsertTags(List<String> tags) async {
|
||||
final dtos = await _repository.upsertTags(tags);
|
||||
if (dtos == null) {
|
||||
return [];
|
||||
}
|
||||
return dtos.map((dto) => Tag.fromDto(dto)).toList();
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,13 @@ class TagsApiRepository extends ApiRepository {
|
||||
Future<List<TagResponseDto>?> getAllTags() async {
|
||||
return await _api.getAllTags();
|
||||
}
|
||||
|
||||
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||
final response = await _api.bulkTagAssets(TagBulkAssetsDto(assetIds: assetIds, tagIds: tagIds));
|
||||
return response?.count ?? 0;
|
||||
}
|
||||
|
||||
Future<List<TagResponseDto>?> upsertTags(List<String> tags) async {
|
||||
return _api.upsertTags(TagUpsertDto(tags: tags));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
expanded: true,
|
||||
onSearch: handleApply,
|
||||
onClear: handleClear,
|
||||
child: TagPicker(onSelect: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()),
|
||||
child: TagPicker(onSelectExistingTag: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
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/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/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class BulkTagAssetsActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
|
||||
const BulkTagAssetsActionButton({super.key, required this.source});
|
||||
|
||||
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||
final result = await ref.read(actionProvider.notifier).tagAssets(source, context);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: result.success
|
||||
? 'tagged_assets'.t(context: context, args: {'count': result.count.toString()})
|
||||
: 'errors.failed_to_tag_assets'.t(context: context),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: result.success ? ToastType.success : ToastType.error,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.sell_outlined,
|
||||
label: "control_bottom_app_bar_add_tags".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
@@ -26,6 +27,7 @@ import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.d
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -57,6 +59,9 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
final tagsEnabled = ref.watch(
|
||||
userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false),
|
||||
);
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
@@ -114,6 +119,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'
|
||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider;
|
||||
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
@@ -353,6 +354,23 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult?> tagAssets(ActionSource source, BuildContext context) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
try {
|
||||
final count = await _service.tagAssets(ids, context);
|
||||
if (count == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ref.invalidate(tagProvider);
|
||||
return ActionResult(count: count, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to tag assets', error, stack);
|
||||
ref.invalidate(tagProvider);
|
||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> removeFromAlbum(ActionSource source, String albumId) async {
|
||||
final ids = _getRemoteIdsForSource(source);
|
||||
try {
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/tags_api.repository.dart';
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
|
||||
class TagNotifier extends AsyncNotifier<Set<Tag>> {
|
||||
@override
|
||||
Future<Set<Tag>> build() async {
|
||||
final repo = ref.read(tagsApiRepositoryProvider);
|
||||
final allTags = await repo.getAllTags();
|
||||
if (allTags == null) {
|
||||
return {};
|
||||
}
|
||||
return allTags.map((t) => Tag.fromDto(t)).toSet();
|
||||
return ref.watch(tagServiceProvider).getAllTags();
|
||||
}
|
||||
|
||||
Future<int> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
|
||||
return ref.read(tagServiceProvider).bulkTagAssets(assetIds, tagIds);
|
||||
}
|
||||
|
||||
Future<List<Tag>> upsertTags(List<String> tags) async {
|
||||
final upsertedTags = await ref.read(tagServiceProvider).upsertTags(tags);
|
||||
|
||||
state = AsyncValue.data({...?state.valueOrNull, ...upsertedTags});
|
||||
return upsertedTags;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
@@ -23,6 +24,7 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/timezone.dart';
|
||||
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||
import 'package:immich_mobile/widgets/common/tag_picker.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
||||
|
||||
final actionServiceProvider = Provider<ActionService>(
|
||||
@@ -35,6 +37,7 @@ final actionServiceProvider = Provider<ActionService>(
|
||||
ref.watch(trashedLocalAssetRepository),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
ref.watch(downloadRepositoryProvider),
|
||||
ref.watch(tagServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -47,6 +50,7 @@ class ActionService {
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final DownloadRepository _downloadRepository;
|
||||
final TagService _tagService;
|
||||
|
||||
const ActionService(
|
||||
this._assetApiRepository,
|
||||
@@ -57,6 +61,7 @@ class ActionService {
|
||||
this._trashedLocalAssetRepository,
|
||||
this._assetMediaRepository,
|
||||
this._downloadRepository,
|
||||
this._tagService,
|
||||
);
|
||||
|
||||
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||
@@ -234,6 +239,26 @@ class ActionService {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<int?> tagAssets(List<String> remoteIds, BuildContext context) async {
|
||||
final tagResults = await showTagPickerModal(context: context);
|
||||
if (tagResults == null) {
|
||||
// user cancelled
|
||||
return null;
|
||||
}
|
||||
|
||||
final selectedTagIds = Set<String>.from(tagResults.$1);
|
||||
final selectedNewTagValues = tagResults.$2;
|
||||
|
||||
if (selectedNewTagValues.isNotEmpty) {
|
||||
final upsertedTags = await _tagService.upsertTags(selectedNewTagValues.toList());
|
||||
selectedTagIds.addAll(upsertedTags.map((t) => t.id));
|
||||
}
|
||||
if (selectedTagIds.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
return _tagService.bulkTagAssets(remoteIds, selectedTagIds.toList());
|
||||
}
|
||||
|
||||
Future<void> stack(String userId, List<String> remoteIds) async {
|
||||
final stack = await _assetApiRepository.stack(remoteIds);
|
||||
await _remoteAssetRepository.stack(userId, stack);
|
||||
|
||||
@@ -8,12 +8,78 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
|
||||
class TagPicker extends HookConsumerWidget {
|
||||
const TagPicker({super.key, required this.onSelect, required this.filter});
|
||||
String _trimSlashes(String s) => s.replaceAll(RegExp(r'^/+|/+$'), '');
|
||||
|
||||
Future<(Set<String>, Set<String>)?> showTagPickerModal({required BuildContext context, Set<String>? initialSelection}) {
|
||||
return showDialog<(Set<String>, Set<String>)?>(
|
||||
context: context,
|
||||
builder: (context) => _TagPickerModal(initialSelection: initialSelection),
|
||||
);
|
||||
}
|
||||
|
||||
class _TagPickerModal extends HookConsumerWidget {
|
||||
final Set<String>? initialSelection;
|
||||
|
||||
const _TagPickerModal({this.initialSelection});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedTagIds = useState<Set<String>>(initialSelection ?? {});
|
||||
final newTagValues = useState<Set<String>>({});
|
||||
|
||||
void onSelectExistingTag(Iterable<Tag> tags) {
|
||||
selectedTagIds.value = tags.map((tag) => tag.id).toSet();
|
||||
}
|
||||
|
||||
void onSelectNewTag(Set<String> tags) {
|
||||
newTagValues.value = tags;
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 0),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(
|
||||
"cancel",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.colorScheme.error,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.pop((selectedTagIds.value, newTagValues.value)),
|
||||
child: Text(
|
||||
"action_common_update",
|
||||
style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
content: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: TagPicker(
|
||||
onSelectExistingTag: onSelectExistingTag,
|
||||
filter: selectedTagIds.value,
|
||||
onSelectNewTag: onSelectNewTag,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TagPicker extends HookConsumerWidget {
|
||||
const TagPicker({super.key, required this.onSelectExistingTag, required this.filter, this.onSelectNewTag});
|
||||
|
||||
final Function(Iterable<Tag>) onSelect;
|
||||
final Set<String> filter;
|
||||
|
||||
/// Callback when existing tags are selected/deselected.
|
||||
final Function(Iterable<Tag>) onSelectExistingTag;
|
||||
|
||||
/// If not null, shows a tile to create a new tag with user's filter input.
|
||||
final Function(Set<String>)? onSelectNewTag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formFocus = useFocusNode();
|
||||
@@ -21,6 +87,7 @@ class TagPicker extends HookConsumerWidget {
|
||||
final tags = ref.watch(tagProvider);
|
||||
final selectedTagIds = useState<Set<String>>(filter);
|
||||
final borderRadius = const BorderRadius.all(Radius.circular(10));
|
||||
final selectedNewTagValues = useState<Set<String>>({});
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -41,13 +108,53 @@ class TagPicker extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: tags.widgetWhen(
|
||||
onData: (tags) {
|
||||
final trimmedQuery = _trimSlashes(searchQuery.value);
|
||||
final queryResult = tags
|
||||
.where((t) => t.value.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
||||
.where((t) => t.value.toLowerCase().contains(trimmedQuery.toLowerCase()))
|
||||
.toList();
|
||||
final showCreateTile =
|
||||
(onSelectNewTag != null) &&
|
||||
trimmedQuery.isNotEmpty &&
|
||||
!tags.any((t) => t.value.toLowerCase() == trimmedQuery.toLowerCase());
|
||||
final isCreateSelected = selectedNewTagValues.value.contains(trimmedQuery);
|
||||
return ListView.builder(
|
||||
itemCount: queryResult.length,
|
||||
itemCount: queryResult.length + (showCreateTile ? 1 : 0),
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemBuilder: (context, index) {
|
||||
if (showCreateTile && index == queryResult.length) {
|
||||
// Create new tag tile
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isCreateSelected ? context.primaryColor : context.primaryColor.withAlpha(25),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
trimmedQuery,
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: isCreateSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.add,
|
||||
color: isCreateSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface,
|
||||
),
|
||||
onTap: () {
|
||||
final newSelectedNewTagValues = {...selectedNewTagValues.value};
|
||||
if (isCreateSelected) {
|
||||
newSelectedNewTagValues.remove(trimmedQuery);
|
||||
} else {
|
||||
newSelectedNewTagValues.add(trimmedQuery);
|
||||
}
|
||||
selectedNewTagValues.value = newSelectedNewTagValues;
|
||||
onSelectNewTag!.call(newSelectedNewTagValues);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final tag = queryResult[index];
|
||||
final isSelected = selectedTagIds.value.any((id) => id == tag.id);
|
||||
|
||||
@@ -73,7 +180,7 @@ class TagPicker extends HookConsumerWidget {
|
||||
newSelected.add(tag.id);
|
||||
}
|
||||
selectedTagIds.value = newSelected;
|
||||
onSelect(tags.where((t) => newSelected.contains(t.id)));
|
||||
onSelectExistingTag(tags.where((t) => newSelected.contains(t.id)));
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/auth.repository.dart';
|
||||
import 'package:immich_mobile/repositories/auth_api.repository.dart';
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
@@ -14,3 +15,5 @@ class MockAuthApiRepository extends Mock implements AuthApiRepository {}
|
||||
class MockAuthRepository extends Mock implements AuthRepository {}
|
||||
|
||||
class MockLocalFilesManagerRepository extends Mock implements LocalFilesManagerRepository {}
|
||||
|
||||
class MockTagService extends Mock implements TagService {}
|
||||
|
||||
@@ -27,6 +27,7 @@ void main() {
|
||||
late MockTrashedLocalAssetRepository trashedLocalAssetRepository;
|
||||
late MockAssetMediaRepository assetMediaRepository;
|
||||
late MockDownloadRepository downloadRepository;
|
||||
late MockTagService tagService;
|
||||
|
||||
late Drift db;
|
||||
|
||||
@@ -53,6 +54,7 @@ void main() {
|
||||
trashedLocalAssetRepository = MockTrashedLocalAssetRepository();
|
||||
assetMediaRepository = MockAssetMediaRepository();
|
||||
downloadRepository = MockDownloadRepository();
|
||||
tagService = MockTagService();
|
||||
|
||||
sut = ActionService(
|
||||
assetApiRepository,
|
||||
@@ -63,6 +65,7 @@ void main() {
|
||||
trashedLocalAssetRepository,
|
||||
assetMediaRepository,
|
||||
downloadRepository,
|
||||
tagService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user