diff --git a/mobile/lib/presentation/pages/search/search.page.dart b/mobile/lib/presentation/pages/search/search.page.dart index a647d2a05..bd7cb2b7c 100644 --- a/mobile/lib/presentation/pages/search/search.page.dart +++ b/mobile/lib/presentation/pages/search/search.page.dart @@ -42,12 +42,16 @@ class SearchPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final serverFeatures = ref.watch(serverInfoProvider.select((v) => v.serverFeatures)); - final textSearchType = useState( - serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename, - ); - final searchHintText = useState( - serverFeatures.smartSearch ? context.t.sunrise_on_the_beach : context.t.file_name_or_extension, - ); + // Stays null until the user picks a type, so the default follows the server features once they are loaded + final selectedTextSearchType = useState(null); + final textSearchType = + selectedTextSearchType.value ?? (serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename); + final searchHintText = switch (textSearchType) { + TextSearchType.context => context.t.sunrise_on_the_beach, + TextSearchType.filename => context.t.file_name_or_extension, + TextSearchType.description => context.t.search_by_description_example, + TextSearchType.ocr => context.t.search_by_ocr_example, + }; final textSearchController = useTextEditingController(); final filter = useState( SearchFilter( @@ -491,14 +495,14 @@ class SearchPage extends HookConsumerWidget { ); } - void handleTextSubmitted(String value) => search(switch (textSearchType.value) { + void handleTextSubmitted(String value) => search(switch (textSearchType) { TextSearchType.context => filter.value.copyWith(filename: '', context: value, description: '', ocr: ''), TextSearchType.filename => filter.value.copyWith(filename: value, context: '', description: '', ocr: ''), TextSearchType.description => filter.value.copyWith(filename: '', context: '', description: value, ocr: ''), TextSearchType.ocr => filter.value.copyWith(filename: '', context: '', description: '', ocr: value), }); - IconData getSearchPrefixIcon() => switch (textSearchType.value) { + IconData getSearchPrefixIcon() => switch (textSearchType) { TextSearchType.context => Icons.image_search_rounded, TextSearchType.filename => Icons.abc_rounded, TextSearchType.description => Icons.text_snippet_outlined, @@ -543,15 +547,14 @@ class SearchPage extends HookConsumerWidget { context.t.search_by_context, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, + color: textSearchType == TextSearchType.context ? context.colorScheme.primary : null, ), ), selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.context, + selected: textSearchType == TextSearchType.context, ), onPressed: () { - textSearchType.value = TextSearchType.context; - searchHintText.value = context.t.sunrise_on_the_beach; + selectedTextSearchType.value = TextSearchType.context; }, ), ), @@ -562,15 +565,14 @@ class SearchPage extends HookConsumerWidget { context.t.search_filter_filename, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, + color: textSearchType == TextSearchType.filename ? context.colorScheme.primary : null, ), ), selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.filename, + selected: textSearchType == TextSearchType.filename, ), onPressed: () { - textSearchType.value = TextSearchType.filename; - searchHintText.value = context.t.file_name_or_extension; + selectedTextSearchType.value = TextSearchType.filename; }, ), MenuItemButton( @@ -580,15 +582,14 @@ class SearchPage extends HookConsumerWidget { context.t.search_by_description, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, + color: textSearchType == TextSearchType.description ? context.colorScheme.primary : null, ), ), selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.description, + selected: textSearchType == TextSearchType.description, ), onPressed: () { - textSearchType.value = TextSearchType.description; - searchHintText.value = context.t.search_by_description_example; + selectedTextSearchType.value = TextSearchType.description; }, ), FeatureCheck( @@ -600,15 +601,14 @@ class SearchPage extends HookConsumerWidget { context.t.search_by_ocr, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null, + color: textSearchType == TextSearchType.ocr ? context.colorScheme.primary : null, ), ), selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.ocr, + selected: textSearchType == TextSearchType.ocr, ), onPressed: () { - textSearchType.value = TextSearchType.ocr; - searchHintText.value = context.t.search_by_ocr_example; + selectedTextSearchType.value = TextSearchType.ocr; }, ), ), @@ -631,7 +631,7 @@ class SearchPage extends HookConsumerWidget { ), ), child: SearchField( - hintText: searchHintText.value, + hintText: searchHintText, key: const Key('search_text_field'), controller: textSearchController, contentPadding: filter.value.assetId != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), diff --git a/mobile/test/presentation/pages/search_page_test.dart b/mobile/test/presentation/pages/search_page_test.dart new file mode 100644 index 000000000..83125e248 --- /dev/null +++ b/mobile/test/presentation/pages/search_page_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/presentation/pages/search/search.page.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; + +import '../../unit/presentation/presentation_context.dart'; + +/// Server info whose features arrive after the page is built, like on a slow or late connection +class DelayedServerInfoNotifier extends ServerInfoNotifier { + DelayedServerInfoNotifier(super.serverInfoService); + + void enableSmartSearch() => state = state.copyWith(serverFeatures: state.serverFeatures.copyWith(smartSearch: true)); +} + +void main() { + late PresentationContext context; + + setUp(() async => context = await PresentationContext.create()); + + tearDown(() async => context.dispose()); + + final contextSearchIcon = find.byIcon(Icons.image_search_rounded); + final filenameSearchIcon = find.byIcon(Icons.abc_rounded); + + Future pumpSearchPage(WidgetTester tester) async { + final serverInfo = DelayedServerInfoNotifier(context.service.serverInfo); + await tester.pumpTestWidget( + context, + const SearchPage(), + overrides: [serverInfoProvider.overrideWith((ref) => serverInfo)], + ); + return serverInfo; + } + + testWidgets('uses context search once smart search is reported by the server', (tester) async { + final serverInfo = await pumpSearchPage(tester); + expect(filenameSearchIcon, findsOneWidget); + + serverInfo.enableSmartSearch(); + await tester.pumpAndSettle(); + + expect(contextSearchIcon, findsOneWidget); + expect(filenameSearchIcon, findsNothing); + }); + + testWidgets('keeps the search type selected by the user', (tester) async { + final serverInfo = await pumpSearchPage(tester); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.text_snippet_outlined)); + await tester.pumpAndSettle(); + + serverInfo.enableSmartSearch(); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.text_snippet_outlined), findsOneWidget); + expect(contextSearchIcon, findsNothing); + }); +}