fix(mobile): use context search once server features are loaded (#31800)
Co-authored-by: Dawid Krynski <188586034+DawidKrynski@users.noreply.github.com> Co-authored-by: Santo Shakil <santoshakil@users.noreply.github.com> (cherry picked from commit be8e42526700d9ec5e72061e33f5039d50e40bae)
This commit is contained in:
@@ -42,12 +42,16 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final serverFeatures = ref.watch(serverInfoProvider.select((v) => v.serverFeatures));
|
final serverFeatures = ref.watch(serverInfoProvider.select((v) => v.serverFeatures));
|
||||||
final textSearchType = useState<TextSearchType>(
|
// Stays null until the user picks a type, so the default follows the server features once they are loaded
|
||||||
serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename,
|
final selectedTextSearchType = useState<TextSearchType?>(null);
|
||||||
);
|
final textSearchType =
|
||||||
final searchHintText = useState<String>(
|
selectedTextSearchType.value ?? (serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename);
|
||||||
serverFeatures.smartSearch ? context.t.sunrise_on_the_beach : context.t.file_name_or_extension,
|
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 textSearchController = useTextEditingController();
|
||||||
final filter = useState<SearchFilter>(
|
final filter = useState<SearchFilter>(
|
||||||
SearchFilter(
|
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.context => filter.value.copyWith(filename: '', context: value, description: '', ocr: ''),
|
||||||
TextSearchType.filename => filter.value.copyWith(filename: value, context: '', description: '', ocr: ''),
|
TextSearchType.filename => filter.value.copyWith(filename: value, context: '', description: '', ocr: ''),
|
||||||
TextSearchType.description => filter.value.copyWith(filename: '', context: '', description: value, ocr: ''),
|
TextSearchType.description => filter.value.copyWith(filename: '', context: '', description: value, ocr: ''),
|
||||||
TextSearchType.ocr => filter.value.copyWith(filename: '', context: '', description: '', ocr: value),
|
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.context => Icons.image_search_rounded,
|
||||||
TextSearchType.filename => Icons.abc_rounded,
|
TextSearchType.filename => Icons.abc_rounded,
|
||||||
TextSearchType.description => Icons.text_snippet_outlined,
|
TextSearchType.description => Icons.text_snippet_outlined,
|
||||||
@@ -543,15 +547,14 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
context.t.search_by_context,
|
context.t.search_by_context,
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null,
|
color: textSearchType == TextSearchType.context ? context.colorScheme.primary : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
selectedColor: context.colorScheme.primary,
|
selectedColor: context.colorScheme.primary,
|
||||||
selected: textSearchType.value == TextSearchType.context,
|
selected: textSearchType == TextSearchType.context,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
textSearchType.value = TextSearchType.context;
|
selectedTextSearchType.value = TextSearchType.context;
|
||||||
searchHintText.value = context.t.sunrise_on_the_beach;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -562,15 +565,14 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
context.t.search_filter_filename,
|
context.t.search_filter_filename,
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null,
|
color: textSearchType == TextSearchType.filename ? context.colorScheme.primary : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
selectedColor: context.colorScheme.primary,
|
selectedColor: context.colorScheme.primary,
|
||||||
selected: textSearchType.value == TextSearchType.filename,
|
selected: textSearchType == TextSearchType.filename,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
textSearchType.value = TextSearchType.filename;
|
selectedTextSearchType.value = TextSearchType.filename;
|
||||||
searchHintText.value = context.t.file_name_or_extension;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
@@ -580,15 +582,14 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
context.t.search_by_description,
|
context.t.search_by_description,
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null,
|
color: textSearchType == TextSearchType.description ? context.colorScheme.primary : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
selectedColor: context.colorScheme.primary,
|
selectedColor: context.colorScheme.primary,
|
||||||
selected: textSearchType.value == TextSearchType.description,
|
selected: textSearchType == TextSearchType.description,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
textSearchType.value = TextSearchType.description;
|
selectedTextSearchType.value = TextSearchType.description;
|
||||||
searchHintText.value = context.t.search_by_description_example;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FeatureCheck(
|
FeatureCheck(
|
||||||
@@ -600,15 +601,14 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
context.t.search_by_ocr,
|
context.t.search_by_ocr,
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null,
|
color: textSearchType == TextSearchType.ocr ? context.colorScheme.primary : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
selectedColor: context.colorScheme.primary,
|
selectedColor: context.colorScheme.primary,
|
||||||
selected: textSearchType.value == TextSearchType.ocr,
|
selected: textSearchType == TextSearchType.ocr,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
textSearchType.value = TextSearchType.ocr;
|
selectedTextSearchType.value = TextSearchType.ocr;
|
||||||
searchHintText.value = context.t.search_by_ocr_example;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -631,7 +631,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SearchField(
|
child: SearchField(
|
||||||
hintText: searchHintText.value,
|
hintText: searchHintText,
|
||||||
key: const Key('search_text_field'),
|
key: const Key('search_text_field'),
|
||||||
controller: textSearchController,
|
controller: textSearchController,
|
||||||
contentPadding: filter.value.assetId != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8),
|
contentPadding: filter.value.assetId != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8),
|
||||||
|
|||||||
@@ -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<DelayedServerInfoNotifier> 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user