From 3b47cb95e965152ed466d0f1a493532cdd20cdd2 Mon Sep 17 00:00:00 2001 From: petlyh <88139840+petlyh@users.noreply.github.com> Date: Sun, 17 Nov 2024 22:01:03 +0100 Subject: [PATCH] Use riverpod for query --- lib/main.dart | 16 ++-- lib/packages/intent_handler.dart | 13 ++- lib/providers/query_provider.dart | 75 +++------------ lib/screens/search/result_page.dart | 12 +-- lib/screens/search/search_screen.dart | 95 ++++++++++--------- .../search_options/radical_search_screen.dart | 51 ++++++++-- .../search_options/search_options_screen.dart | 40 -------- .../search_options/tag_selection_screen.dart | 89 ++++++++--------- lib/widgets/search_field.dart | 45 +++++++++ pubspec.lock | 32 +++++++ pubspec.yaml | 2 + 11 files changed, 252 insertions(+), 218 deletions(-) delete mode 100644 lib/screens/search_options/search_options_screen.dart create mode 100644 lib/widgets/search_field.dart diff --git a/lib/main.dart b/lib/main.dart index 8a4edab4..68196b93 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,8 @@ import "package:dynamic_color/dynamic_color.dart"; import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; -import "package:jsdict/providers/query_provider.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart" + hide ChangeNotifierProvider, Provider; import "package:jsdict/providers/theme_provider.dart"; import "package:jsdict/screens/search/search_screen.dart"; import "package:jsdict/singletons.dart"; @@ -76,12 +77,9 @@ class JsDictApp extends StatelessWidget { @override Widget build(BuildContext context) { return DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) { - return MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => QueryProvider()), - ChangeNotifierProvider(create: (_) => ThemeProvider()), - ], + builder: (lightDynamic, darkDynamic) => ProviderScope( + child: MultiProvider( + providers: [ChangeNotifierProvider(create: (_) => ThemeProvider())], builder: (context, _) { final themeProvider = Provider.of(context); @@ -100,8 +98,8 @@ class JsDictApp extends StatelessWidget { home: const SearchScreen(), ); }, - ); - }, + ), + ), ); } } diff --git a/lib/packages/intent_handler.dart b/lib/packages/intent_handler.dart index 4edcb7e9..d9dc3348 100644 --- a/lib/packages/intent_handler.dart +++ b/lib/packages/intent_handler.dart @@ -14,14 +14,19 @@ import "package:jsdict/screens/word_details/word_details_screen.dart"; import "package:jsdict/widgets/error_indicator.dart"; import "package:receive_sharing_intent/receive_sharing_intent.dart"; -void useIntentHandler({required TabController tabController}) => use( - _IntentHandlerHook(tabController), +void useIntentHandler({ + required TabController tabController, + required QueryController queryController, +}) => + use( + _IntentHandlerHook(tabController, queryController), ); class _IntentHandlerHook extends Hook { - const _IntentHandlerHook(this.tabController); + const _IntentHandlerHook(this.tabController, this.queryController); final TabController tabController; + final QueryController queryController; @override HookState createState() => @@ -100,7 +105,7 @@ class _IntentHandlerHookState extends HookState { } hook.tabController.index = _tabIndex(query); - QueryProvider.of(context).query = removeTags(query); + hook.queryController.update(removeTags(query)); popAll(context); } diff --git a/lib/providers/query_provider.dart b/lib/providers/query_provider.dart index 14a59850..00b9affc 100644 --- a/lib/providers/query_provider.dart +++ b/lib/providers/query_provider.dart @@ -1,72 +1,19 @@ -import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart" hide Provider; import "package:jsdict/packages/remove_tags.dart"; -import "package:provider/provider.dart"; -class QueryProvider extends ChangeNotifier { - static QueryProvider of(BuildContext context) { - return Provider.of(context, listen: false); - } - - TextEditingController searchController = TextEditingController(); - - String _query = ""; - String get query => _query; +final queryProvider = + NotifierProvider(QueryController.new); - set query(String text) { - searchController.text = text; - updateQuery(); - } - - void sanitizeText() { - searchController.text = removeTypeTags(searchController.text) - .trim() - .replaceAll(RegExp(r"\s+"), " "); - } - - void updateQuery() { - sanitizeText(); - _query = searchController.text; - notifyListeners(); - } - - void updateQueryIfChanged() { - if (_query != searchController.text) { - updateQuery(); - } - } - - void addTag(String tag) { - sanitizeText(); - if (searchController.text.isNotEmpty) { - searchController.text += " "; - } - searchController.text += "#$tag"; - } - - void clearTags() { - searchController.text = removeTags(searchController.text); - sanitizeText(); - } - - void insertText(String text) { - final selection = searchController.selection; - final selectionStart = selection.baseOffset; +class QueryController extends Notifier { + @override + String build() => ""; - if (selectionStart == -1) { - searchController.text += text; - return; + void update(String newQuery) { + if (state != newQuery) { + state = _sanitizeQuery(newQuery); } - - final newText = searchController.text - .replaceRange(selectionStart, selection.extentOffset, text); - searchController.text = newText; - searchController.selection = - TextSelection.collapsed(offset: selectionStart + 1); } - @override - void dispose() { - searchController.dispose(); - super.dispose(); - } + String _sanitizeQuery(String query) => + removeTypeTags(query).trim().replaceAll(RegExp(r"\s+"), " "); } diff --git a/lib/screens/search/result_page.dart b/lib/screens/search/result_page.dart index 74f2faf7..c27ab4ce 100644 --- a/lib/screens/search/result_page.dart +++ b/lib/screens/search/result_page.dart @@ -4,6 +4,7 @@ import "package:collection/collection.dart"; import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:infinite_scroll_pagination/infinite_scroll_pagination.dart"; import "package:jsdict/jp_text.dart"; import "package:jsdict/models/models.dart"; @@ -20,7 +21,6 @@ import "package:jsdict/widgets/items/name_item.dart"; import "package:jsdict/widgets/items/sentence_item.dart"; import "package:jsdict/widgets/items/word_item.dart"; import "package:jsdict/widgets/link_span.dart"; -import "package:provider/provider.dart"; class ResultPageScreen extends StatelessWidget { const ResultPageScreen({required this.query}); @@ -239,13 +239,13 @@ class _ConversionText extends StatelessWidget { } } -class _CorrectionText extends StatelessWidget { +class _CorrectionText extends ConsumerWidget { const _CorrectionText({required this.correction}); final Correction correction; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final textColor = Theme.of(context).textTheme.bodyLarge!.color; return _paddedSliverAdapter( @@ -267,9 +267,9 @@ class _CorrectionText extends StatelessWidget { context: context, text: correction.original, bold: true, - onTap: () => Provider.of(context, listen: false) - ..searchController.text = correction.original - ..updateQuery(), + onTap: () => ref + .read(queryProvider.notifier) + .update(correction.original), ), ], ], diff --git a/lib/screens/search/search_screen.dart b/lib/screens/search/search_screen.dart index fa7c9b81..3e5ffff0 100644 --- a/lib/screens/search/search_screen.dart +++ b/lib/screens/search/search_screen.dart @@ -1,6 +1,6 @@ import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; -import "package:jsdict/jp_text.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:jsdict/models/models.dart"; import "package:jsdict/packages/intent_handler.dart"; import "package:jsdict/packages/navigation.dart"; @@ -9,56 +9,46 @@ import "package:jsdict/screens/search/result_page.dart"; import "package:jsdict/screens/search_options/radical_search_screen.dart"; import "package:jsdict/screens/search_options/tag_selection_screen.dart"; import "package:jsdict/screens/settings_screen.dart"; -import "package:provider/provider.dart"; +import "package:jsdict/widgets/search_field.dart"; -class SearchScreen extends HookWidget { +class SearchScreen extends HookConsumerWidget { const SearchScreen(); - static const _placeholder = Center( - child: Text("JS-Dict", style: TextStyle(fontSize: 32)), - ); - @override - Widget build(BuildContext context) { - final queryProvider = QueryProvider.of(context); + Widget build(BuildContext context, WidgetRef ref) { + final searchController = useTextEditingController(text: ""); - final searchController = queryProvider.searchController; - final searchFocusNode = useFocusNode(); + // Update search field text when query is changed from somewhere else. + ref.listen(queryProvider, (_, query) => searchController.text = query); final tabController = useTabController(initialLength: 4); - useIntentHandler(tabController: tabController); + useIntentHandler( + tabController: tabController, + queryController: ref.read(queryProvider.notifier), + ); return Scaffold( resizeToAvoidBottomInset: false, floatingActionButton: FloatingActionButton( - onPressed: pushScreen(context, const RadicalSearchScreen()), + onPressed: () => + pushScreen(context, RadicalSearchScreen(searchController.text)) + .call(), tooltip: "Radicals", child: const Text("部", style: TextStyle(fontSize: 20)), ), appBar: AppBar( - title: TextField( - style: jpTextStyle, - focusNode: searchFocusNode, + title: SearchField( controller: searchController, - onSubmitted: (_) => queryProvider.updateQuery(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search), - border: InputBorder.none, - hintText: "Search...", - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - searchFocusNode.requestFocus(); - searchController.clear(); - }, - tooltip: "Clear", - ), - ), + onSubmitted: ref.read(queryProvider.notifier).update, + showSearchIcon: true, + focusOnClear: true, ), actions: [ IconButton( - onPressed: pushScreen(context, const TagSelectionScreen()), + onPressed: () => + pushScreen(context, TagSelectionScreen(searchController.text)) + .call(), icon: const Icon(Icons.tag), tooltip: "Tags", ), @@ -80,19 +70,36 @@ class SearchScreen extends HookWidget { ], ), ), - body: Consumer( - builder: (_, provider, __) => provider.query.isEmpty - ? _placeholder - : TabBarView( - controller: tabController, - children: [ - ResultPage(query: provider.query, key: UniqueKey()), - ResultPage(query: provider.query, key: UniqueKey()), - ResultPage(query: provider.query, key: UniqueKey()), - ResultPage(query: provider.query, key: UniqueKey()), - ], - ), - ), + body: _SearchScreenContent(tabController: tabController), + ); + } +} + +class _SearchScreenContent extends ConsumerWidget { + const _SearchScreenContent({required this.tabController}); + + final TabController tabController; + + static const _placeholder = Center( + child: Text("JS-Dict", style: TextStyle(fontSize: 32)), + ); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final query = ref.watch(queryProvider); + + if (query.isEmpty) { + return _placeholder; + } + + return TabBarView( + controller: tabController, + children: [ + ResultPage(query: query, key: ValueKey((query, Word))), + ResultPage(query: query, key: ValueKey((query, Kanji))), + ResultPage(query: query, key: ValueKey((query, Name))), + ResultPage(query: query, key: ValueKey((query, Sentence))), + ], ); } } diff --git a/lib/screens/search_options/radical_search_screen.dart b/lib/screens/search_options/radical_search_screen.dart index 69460d6a..d367e401 100644 --- a/lib/screens/search_options/radical_search_screen.dart +++ b/lib/screens/search_options/radical_search_screen.dart @@ -1,22 +1,59 @@ import "package:collection/collection.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:jsdict/jp_text.dart"; import "package:jsdict/packages/radical_search/radical_search.dart"; import "package:jsdict/providers/query_provider.dart"; -import "package:jsdict/screens/search_options/search_options_screen.dart"; +import "package:jsdict/widgets/search_field.dart"; -class RadicalSearchScreen extends SearchOptionsScreen { - const RadicalSearchScreen() : super(body: const _RadicalSearch()); +class RadicalSearchScreen extends HookConsumerWidget { + const RadicalSearchScreen(this.initialQuery); + + final String initialQuery; + + void _insertText(TextEditingController controller, String text) { + final selection = controller.selection; + final selectionStart = selection.baseOffset; + + if (selectionStart == -1) { + controller.text += text; + return; + } + + final newText = controller.text + .replaceRange(selectionStart, selection.extentOffset, text); + controller.text = newText; + controller.selection = TextSelection.collapsed(offset: selectionStart + 1); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(text: initialQuery); + + return PopScope( + onPopInvokedWithResult: (_, __) => + ref.read(queryProvider.notifier).update(controller.text), + child: Scaffold( + appBar: AppBar( + title: SearchField(controller: controller), + scrolledUnderElevation: 0, + ), + body: _RadicalSearch( + onSelectKanji: (kanji) => _insertText(controller, kanji), + ), + ), + ); + } } class _RadicalSearch extends HookWidget { - const _RadicalSearch(); + const _RadicalSearch({required this.onSelectKanji}); + + final void Function(String kanji) onSelectKanji; @override Widget build(BuildContext context) { - final queryProvider = QueryProvider.of(context); - final selectedRadicals = useState([]); final matchingKanji = selectedRadicals.value.isNotEmpty @@ -30,7 +67,7 @@ class _RadicalSearch extends HookWidget { matches: matchingKanji, onSelect: (kanji) { selectedRadicals.value = []; - queryProvider.insertText(kanji); + onSelectKanji(kanji); }, onReset: () => selectedRadicals.value = [], ), diff --git a/lib/screens/search_options/search_options_screen.dart b/lib/screens/search_options/search_options_screen.dart deleted file mode 100644 index eedb0a8e..00000000 --- a/lib/screens/search_options/search_options_screen.dart +++ /dev/null @@ -1,40 +0,0 @@ -import "package:flutter/material.dart"; -import "package:jsdict/jp_text.dart"; -import "package:jsdict/providers/query_provider.dart"; - -class SearchOptionsScreen extends StatelessWidget { - const SearchOptionsScreen({required this.body, this.floatingActionButton}); - - final Widget body; - final Widget? floatingActionButton; - - @override - Widget build(BuildContext context) { - final queryProvider = QueryProvider.of(context); - final searchController = queryProvider.searchController; - - return PopScope( - onPopInvokedWithResult: (_, __) => queryProvider.updateQueryIfChanged(), - child: Scaffold( - floatingActionButton: floatingActionButton, - appBar: AppBar( - scrolledUnderElevation: 0, - title: TextField( - style: jpTextStyle, - controller: searchController, - decoration: InputDecoration( - border: InputBorder.none, - hintText: "Search...", - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: searchController.clear, - tooltip: "Clear", - ), - ), - ), - ), - body: body, - ), - ); - } -} diff --git a/lib/screens/search_options/tag_selection_screen.dart b/lib/screens/search_options/tag_selection_screen.dart index 99ea8381..85539514 100644 --- a/lib/screens/search_options/tag_selection_screen.dart +++ b/lib/screens/search_options/tag_selection_screen.dart @@ -1,56 +1,57 @@ import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:jsdict/packages/remove_tags.dart"; import "package:jsdict/packages/tags.dart"; import "package:jsdict/providers/query_provider.dart"; -import "package:jsdict/screens/search_options/search_options_screen.dart"; import "package:jsdict/widgets/info_chip.dart"; +import "package:jsdict/widgets/search_field.dart"; -class TagSelectionScreen extends SearchOptionsScreen { - const TagSelectionScreen() - : super( - body: const _TagSelection(), - floatingActionButton: const _TagFAB(), - ); -} - -class _TagFAB extends StatelessWidget { - const _TagFAB(); - - @override - Widget build(BuildContext context) { - return FloatingActionButton( - onPressed: QueryProvider.of(context).clearTags, - tooltip: "Clear Tags", - child: const Icon(Icons.clear), - ); - } -} +class TagSelectionScreen extends HookConsumerWidget { + const TagSelectionScreen(this.initialQuery); -class _TagSelection extends StatelessWidget { - const _TagSelection(); + final String initialQuery; @override - Widget build(BuildContext context) { - final queryProvider = QueryProvider.of(context); + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(text: initialQuery); - return SingleChildScrollView( - child: Column( - children: wordTags.entries - .map( - (entry) => ListTile( - title: Text(entry.key), - subtitle: Wrap( - children: entry.value.entries - .map( - (tagEntry) => InfoChip( - tagEntry.key, - onTap: () => queryProvider.addTag(tagEntry.value), - ), - ) - .toList(), - ), - ), - ) - .toList(), + return PopScope( + onPopInvokedWithResult: (_, __) => + ref.read(queryProvider.notifier).update(controller.text), + child: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () => controller.text = removeTags(controller.text), + tooltip: "Clear Tags", + child: const Icon(Icons.clear), + ), + appBar: AppBar( + title: SearchField(controller: controller), + scrolledUnderElevation: 0, + ), + body: SingleChildScrollView( + child: Column( + children: wordTags.entries + .map( + (entry) => ListTile( + title: Text(entry.key), + subtitle: Wrap( + children: entry.value.entries + .map( + (tagEntry) => InfoChip( + tagEntry.key, + onTap: () => controller.text = + "${controller.text} #${tagEntry.value}" + .trim(), + ), + ) + .toList(), + ), + ), + ) + .toList(), + ), + ), ), ); } diff --git a/lib/widgets/search_field.dart b/lib/widgets/search_field.dart new file mode 100644 index 00000000..909e5acf --- /dev/null +++ b/lib/widgets/search_field.dart @@ -0,0 +1,45 @@ +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:jsdict/jp_text.dart"; + +class SearchField extends HookWidget { + const SearchField({ + required this.controller, + this.onSubmitted, + this.focusOnClear = false, + this.showSearchIcon = false, + }); + + final TextEditingController controller; + final bool focusOnClear; + final bool showSearchIcon; + final void Function(String)? onSubmitted; + + @override + Widget build(BuildContext context) { + final focusNode = useFocusNode(); + + return TextField( + style: jpTextStyle, + focusNode: focusNode, + controller: controller, + onSubmitted: onSubmitted, + decoration: InputDecoration( + prefixIcon: showSearchIcon ? const Icon(Icons.search) : null, + border: InputBorder.none, + hintText: "Search...", + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + if (focusOnClear) { + focusNode.requestFocus(); + } + + controller.clear(); + }, + tooltip: "Clear", + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 074236db..b927c59d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -395,6 +395,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" flutter_staggered_grid_view: dependency: transitive description: @@ -485,6 +493,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966" + url: "https://pub.dev" + source: hosted + version: "2.6.1" html: dependency: "direct main" description: @@ -813,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" ruby_text: dependency: "direct main" description: @@ -987,6 +1011,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1186f1bb..17818c2a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,10 +17,12 @@ dependencies: flutter: sdk: flutter flutter_hooks: ^0.18.6 + flutter_riverpod: ^2.6.1 flutter_svg: ^2.0.9 fpdart: ^1.1.0 freezed_annotation: ^2.4.4 get_it: ^8.0.0 + hooks_riverpod: ^2.6.1 html: ^0.15.3 http: ^1.0.0 infinite_scroll_pagination: ^4.0.0