feat(mobile): mobile tags (#30753)

* feat(mobile): mobile tags

* fix(mobile); refactoring, replaced hook

* tag provider through asset service

* make search use a tag set

* use tag action directly

* use notification listener

* untag action

* handle untag response

* fix tests and add untag action test

* fix material error from tag picker dialog

* fix(mobile): clip tag picker tiles and update tag overflow

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Santo Shakil <shakil.mezbah@gmail.com>
(cherry picked from commit 83c7ec50056bbefa10869c78457f8882c72275e2)
This commit is contained in:
Will
2026-10-14 21:24:13 +03:00
committed by Codex
parent 4fe7d42cfc
commit 1daf922555
14 changed files with 363 additions and 66 deletions
+4
View File
@@ -200,6 +200,7 @@ class ServiceMocks {
void _stubTagApi() {
when(tag.bulkTagAssets).thenAnswer((_) async => 0);
when(tag.untagAssets).thenAnswer((_) async => 0);
when(tag.upsert).thenAnswer((_) async => const []);
when(tag.getAll).thenAnswer((_) async => const []);
}
@@ -446,6 +447,9 @@ extension type const TagApiRepositoryStub(MockTagApiRepository repo) implements
Future<int> Function() get bulkTagAssets =>
() => repo.bulkTagAssets(any(), any());
Future<int> Function() get untagAssets =>
() => repo.untagAssets(any(), any());
Future<List<Tag>> Function() get upsert =>
() => repo.upsert(any());
@@ -153,4 +153,44 @@ void main() {
verifyNever(() => context.service.toast.success(any()));
});
});
group('UntagAction', () {
List<Override> untagOverrides(Set<BaseAsset> selection) => [
...context.selected(selection),
toastServiceProvider.overrideWithValue(context.service.toast),
Store.userMetadata.preferences().overrideWith((ref) => Stream.value(const .new(tagsEnabled: true))),
];
testWidgets('removes the tag from the owned assets and reports the count', (tester) async {
final asset = owned();
when(context.service.tag.untagAssets).thenAnswer((_) async => 1);
await tester.pumpTestAction(
context,
const UntagAction(source: .timeline, tagId: 'tag'),
overrides: untagOverrides({asset}),
);
await tester.pumpAndSettle();
verify(() => tagApi.untagAssets('tag', [asset.id])).called(1);
final message = verify(() => context.service.toast.success(captureAny())).captured.single as String;
expect(message, StaticTranslations.instance.removed_tagged_assets(count: 1));
});
testWidgets('is hidden for assets owned by someone else', (tester) async {
final selection = {RemoteAssetFactory.create()};
await tester.pumpTestWidget(
context,
const ActionIconButton(
action: UntagAction(source: .timeline, tagId: 'tag'),
),
overrides: untagOverrides(selection),
);
expect(find.byType(ImmichIconButton), findsNothing);
verifyNever(() => tagApi.untagAssets(any(), any()));
});
});
}