Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,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",
Expand Down Expand Up @@ -1070,6 +1071,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",
Expand Down
31 changes: 31 additions & 0 deletions mobile/lib/domain/services/tag.service.dart
Original file line number Diff line number Diff line change
@@ -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<void> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
await _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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@ class TagsApiRepository extends ApiRepository {
Future<List<TagResponseDto>?> getAllTags() async {
return await _api.getAllTags();
}

Future<void> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
await _api.bulkTagAssets(TagBulkAssetsDto(assetIds: assetIds, tagIds: tagIds));
}

Future<List<TagResponseDto>?> upsertTags(List<String> tags) async {
return _api.upsertTags(TagUpsertDto(tags: tags));
}
}
7 changes: 3 additions & 4 deletions mobile/lib/presentation/pages/search/drift_search.page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ class DriftSearchPage extends HookConsumerWidget {
expanded: true,
onSearch: search,
onClear: handleClear,
child: TagPicker(onSelect: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()),
child: TagPicker(onSelectExistingTag: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()),
),
),
);
Expand Down Expand Up @@ -696,7 +696,7 @@ class DriftSearchPage extends HookConsumerWidget {
label: 'search_filter_location'.t(context: context),
currentFilter: locationCurrentFilterWidget.value,
),
if (userPreferences.value?.tagsEnabled ?? false)
if (userPreferences.valueOrNull?.tagsEnabled ?? false)
SearchFilterChip(
icon: Icons.sell_outlined,
onTap: showTagPicker,
Expand All @@ -722,14 +722,13 @@ class DriftSearchPage extends HookConsumerWidget {
label: 'search_filter_media_type'.t(context: context),
currentFilter: mediaTypeCurrentFilterWidget.value,
),
if (userPreferences.value?.ratingsEnabled ?? false) ...[
if (userPreferences.valueOrNull?.ratingsEnabled ?? false)
SearchFilterChip(
icon: Icons.star_outline_rounded,
onTap: showStarRatingPicker,
label: 'search_filter_star_rating'.t(context: context),
currentFilter: ratingCurrentFilterWidget.value,
),
],
SearchFilterChip(
icon: Icons.display_settings_outlined,
onTap: showDisplayOptionPicker,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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});

_onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}

final result = await ref.read(actionProvider.notifier).tagAssets(source, context);
if (result == null) {
return;
}

ref.read(multiSelectProvider.notifier).reset();

final successMessage = 'tagged_assets'.t(context: context, args: {'count': result.count.toString()});

if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success ? successMessage : '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(
maxWidth: 95.0,
iconData: Icons.sell,
label: "control_bottom_app_bar_add_tags".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -114,6 +115,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
: const DeletePermanentActionButton(source: ActionSource.timeline),
const FavoriteActionButton(source: ActionSource.timeline),
const ArchiveActionButton(source: ActionSource.timeline),
const BulkTagAssetsActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
Expand Down
15 changes: 15 additions & 0 deletions mobile/lib/providers/infrastructure/action.provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,21 @@ class ActionNotifier extends Notifier<void> {
}
}

Future<ActionResult?> tagAssets(ActionSource source, BuildContext context) async {
final ids = _getOwnedRemoteIdsForSource(source);
try {
final isTagged = await _service.tagAssets(ids, context);
if (!isTagged) {
return null;
}

return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to tag assets', error, stack);
return ActionResult(count: ids.length, success: false, error: error.toString());
}
}

Future<ActionResult> removeFromAlbum(ActionSource source, String albumId) async {
final ids = _getRemoteIdsForSource(source);
try {
Expand Down
23 changes: 16 additions & 7 deletions mobile/lib/providers/infrastructure/tag.provider.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
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>> {
late final TagService service;

@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();
final service = ref.watch(tagServiceProvider);
return await service.getAllTags();
}

Future<void> bulkTagAssets(List<String> assetIds, List<String> tagIds) async {
await service.bulkTagAssets(assetIds, tagIds);
}

Future<List<Tag>> upsertTags(List<String> tags) async {
final upsertedTags = await service.upsertTags(tags);

state = AsyncValue.data({...?state.valueOrNull, ...upsertedTags});
return upsertedTags;
}
}

Expand Down
25 changes: 25 additions & 0 deletions mobile/lib/services/action.service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:immich_mobile/infrastructure/repositories/remote_asset.repositor
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/download.repository.dart';
Expand All @@ -22,6 +23,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;
import 'package:riverpod_annotation/riverpod_annotation.dart';

Expand All @@ -35,6 +37,7 @@ final actionServiceProvider = Provider<ActionService>(
ref.watch(trashedLocalAssetRepository),
ref.watch(assetMediaRepositoryProvider),
ref.watch(downloadRepositoryProvider),
ref.watch(tagProvider.notifier),
),
);

Expand All @@ -47,6 +50,7 @@ class ActionService {
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final AssetMediaRepository _assetMediaRepository;
final DownloadRepository _downloadRepository;
final TagNotifier _tagService;

const ActionService(
this._assetApiRepository,
Expand All @@ -57,6 +61,7 @@ class ActionService {
this._trashedLocalAssetRepository,
this._assetMediaRepository,
this._downloadRepository,
this._tagService,
);

Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
Expand Down Expand Up @@ -222,6 +227,26 @@ class ActionService {
return true;
}

Future<bool> tagAssets(List<String> remoteIds, BuildContext context) async {
final tagResults = await showTagPickerModal(context: context);
if (tagResults == null) {
// user cancelled
return false;
}

final selectedTagIds = 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.isNotEmpty) {
await _tagService.bulkTagAssets(remoteIds, selectedTagIds.toList());
}
return true;
}

Future<void> stack(String userId, List<String> remoteIds) async {
final stack = await _assetApiRepository.stack(remoteIds);
await _remoteAssetRepository.stack(userId, stack);
Expand Down
Loading
Loading