diff --git a/lib/bloc/kanji/kanji_bloc.dart b/lib/bloc/kanji/kanji_bloc.dart index 593baf2..a8cfe7d 100644 --- a/lib/bloc/kanji/kanji_bloc.dart +++ b/lib/bloc/kanji/kanji_bloc.dart @@ -5,6 +5,7 @@ import './kanji_state.dart'; import 'package:bloc/bloc.dart'; import 'package:jisho_study_tool/services/kanji_search.dart'; +import 'package:jisho_study_tool/services/kanji_suggestions.dart'; export './kanji_event.dart'; export './kanji_state.dart'; @@ -28,6 +29,10 @@ class KanjiBloc extends Bloc { } on Exception { yield KanjiSearchError('Something went wrong'); } + } else if (event is GetKanjiSuggestions) { + + final suggestions = kanjiSuggestions(event.searchString); + yield KanjiSearchInput(suggestions); } else if (event is ReturnToInitialState) { yield KanjiSearchInitial(); diff --git a/lib/bloc/kanji/kanji_event.dart b/lib/bloc/kanji/kanji_event.dart index 6bbe8e5..7bb79b8 100644 --- a/lib/bloc/kanji/kanji_event.dart +++ b/lib/bloc/kanji/kanji_event.dart @@ -2,12 +2,16 @@ abstract class KanjiEvent { const KanjiEvent(); } +class GetKanjiSuggestions extends KanjiEvent { + final String searchString; + const GetKanjiSuggestions(this.searchString); +} + class GetKanji extends KanjiEvent { final String kanjiSearchString; - - GetKanji(this.kanjiSearchString); + const GetKanji(this.kanjiSearchString); } class ReturnToInitialState extends KanjiEvent { - ReturnToInitialState(); + const ReturnToInitialState(); } \ No newline at end of file diff --git a/lib/bloc/kanji/kanji_state.dart b/lib/bloc/kanji/kanji_state.dart index 7e5aff2..ad835db 100644 --- a/lib/bloc/kanji/kanji_state.dart +++ b/lib/bloc/kanji/kanji_state.dart @@ -5,18 +5,23 @@ abstract class KanjiState { } class KanjiSearchInitial extends KanjiState { - KanjiSearchInitial(); + const KanjiSearchInitial(); +} + +class KanjiSearchInput extends KanjiState { + final List kanjiSuggestions; + const KanjiSearchInput(this.kanjiSuggestions); } class KanjiSearchLoading extends KanjiState { - KanjiSearchLoading(); + const KanjiSearchLoading(); } class KanjiSearchFinished extends KanjiState { final KanjiResult kanji; final bool starred; - KanjiSearchFinished({ + const KanjiSearchFinished({ this.kanji, this.starred = false, }); @@ -25,7 +30,5 @@ class KanjiSearchFinished extends KanjiState { class KanjiSearchError extends KanjiState { final String message; - KanjiSearchError(this.message); -} - -class ReKanjiSearch extends KanjiState {} + const KanjiSearchError(this.message); +} \ No newline at end of file diff --git a/lib/bloc/search/search_bloc.dart b/lib/bloc/search/search_bloc.dart index 1cd2b14..966d290 100644 --- a/lib/bloc/search/search_bloc.dart +++ b/lib/bloc/search/search_bloc.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; part 'search_event.dart'; diff --git a/lib/bloc/search/search_state.dart b/lib/bloc/search/search_state.dart index 87f3d6a..8e79f14 100644 --- a/lib/bloc/search/search_state.dart +++ b/lib/bloc/search/search_state.dart @@ -9,6 +9,4 @@ class SearchLoading extends SearchState {} class SearchFinished extends SearchState {} -class SearchError extends SearchState {} - -class ReSearch extends SearchState {} \ No newline at end of file +class SearchError extends SearchState {} \ No newline at end of file diff --git a/lib/components/kanji/kanji__search_page/examples.dart b/lib/components/kanji/kanji__search_page/examples.dart new file mode 100644 index 0000000..f90911a --- /dev/null +++ b/lib/components/kanji/kanji__search_page/examples.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:unofficial_jisho_api/api.dart'; + +class Examples extends StatelessWidget { + final List _onyomiExamples; + final List _kunyomiExamples; + + const Examples( + this._onyomiExamples, + this._kunyomiExamples, + ); + + @override + Widget build(BuildContext context) { + return ExpansionTile( + title: Center( + child: Container( + padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(10.0), + ), + child: Text( + 'Examples', + style: TextStyle( + color: Colors.white, + fontSize: 20.0, + ), + ), + ), + ), + children: [ + _onyomiExamples + .map((onyomiExample) => _Example(onyomiExample, _KanaType.onyomi)) + .toList(), + _kunyomiExamples + .map((kunyomiExample) => + _Example(kunyomiExample, _KanaType.kunyomi)) + .toList(), + ].expand((list) => list).toList()); + } +} + +enum _KanaType { kunyomi, onyomi } + +class _Example extends StatelessWidget { + final _KanaType _kanaType; + final YomiExample _yomiExample; + + const _Example(this._yomiExample, this._kanaType); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 10.0, + ), + decoration: BoxDecoration( + color: Colors.grey, borderRadius: BorderRadius.circular(10.0)), + child: IntrinsicHeight( + child: Row( + children: [ + Container( + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 10.0, + ), + decoration: BoxDecoration( + color: (_kanaType == _KanaType.kunyomi) + ? Colors.lightBlue + : Colors.orange, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + bottomLeft: Radius.circular(10.0), + ), + ), + child: Column( + children: [ + Container( + child: Text( + _yomiExample.reading, + style: TextStyle( + color: Colors.white, + fontSize: 15.0, + ), + ), + ), + SizedBox( + height: 5.0, + ), + Container( + child: Text( + _yomiExample.example, + style: TextStyle( + color: Colors.white, + fontSize: 20.0, + ), + ), + ), + ], + ), + ), + SizedBox( + width: 15.0, + ), + Expanded( + child: Wrap( + children: [ + Container( + child: Text( + _yomiExample.meaning, + style: TextStyle( + color: Colors.white, + ), + ), + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/kanji/kanji__search_page/kanji_search_page.dart b/lib/components/kanji/kanji__search_page/kanji_search_page.dart index 2e7cead..3c054ed 100644 --- a/lib/components/kanji/kanji__search_page/kanji_search_page.dart +++ b/lib/components/kanji/kanji__search_page/kanji_search_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:jisho_study_tool/components/kanji/kanji__search_page/examples.dart'; import 'package:unofficial_jisho_api/api.dart' as jisho; @@ -81,6 +82,7 @@ class KanjiResultCard extends StatelessWidget { ], ), ), + Examples(_result.onyomiExamples, _result.kunyomiExamples), ], ); } diff --git a/lib/components/kanji/kanji_suggestions.dart b/lib/components/kanji/kanji_suggestions.dart new file mode 100644 index 0000000..ade96cb --- /dev/null +++ b/lib/components/kanji/kanji_suggestions.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:jisho_study_tool/bloc/kanji/kanji_bloc.dart'; + +class KanjiSuggestions extends StatelessWidget { + final List _suggestions; + const KanjiSuggestions(this._suggestions); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.grey[300], + padding: EdgeInsets.symmetric( + vertical: 20.0, + horizontal: 40.0, + ), + child: GridView.count( + crossAxisCount: 3, + mainAxisSpacing: 20.0, + crossAxisSpacing: 40.0, + children: _suggestions.map((kanji) => _Suggestion(kanji)).toList(), + ), + ); + } +} + +class _Suggestion extends StatelessWidget { + final String _kanji; + const _Suggestion(this._kanji); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + BlocProvider.of(context).add(GetKanji(_kanji)); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(10.0), + ), + child: Container( + margin: EdgeInsets.all(10.0), + child: FittedBox( + child: Text( + _kanji, + style: TextStyle(color: Colors.white), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/kanji_search.dart b/lib/screens/kanji_search.dart index 03f9100..9ed202f 100644 --- a/lib/screens/kanji_search.dart +++ b/lib/screens/kanji_search.dart @@ -1,28 +1,42 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:jisho_study_tool/bloc/kanji/kanji_bloc.dart'; import 'package:jisho_study_tool/components/kanji/kanji__search_page/kanji_search_page.dart'; +import 'package:jisho_study_tool/components/kanji/kanji_suggestions.dart'; import 'package:jisho_study_tool/components/loading.dart'; class KanjiView extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is KanjiSearchInitial) - return Container(); - else if (state is KanjiSearchLoading) - return LoadingScreen(); - else if (state is KanjiSearchFinished) - return WillPopScope( - child: KanjiResultCard(state.kanji), - onWillPop: () async { - BlocProvider.of(context).add(ReturnToInitialState()); - return false; - }); - throw 'No such event found'; + return BlocListener( + listener: (context, state) { + if (state is KanjiSearchInitial) { + FocusScope.of(context).unfocus(); + } else if (state is KanjiSearchLoading) { + FocusScope.of(context).unfocus(); + } }, + child: BlocBuilder( + builder: (context, state) { + if (state is KanjiSearchInitial) { + return Container(); + } else if (state is KanjiSearchInput) + return KanjiSuggestions(state.kanjiSuggestions); + else if (state is KanjiSearchLoading) + return LoadingScreen(); + else if (state is KanjiSearchFinished) + return WillPopScope( + child: KanjiResultCard(state.kanji), + onWillPop: () async { + BlocProvider.of(context) + .add(ReturnToInitialState()); + return false; + }); + throw 'No such event found'; + }, + ), ); } } @@ -35,24 +49,12 @@ class KanjiViewBar extends StatelessWidget { children: [ IconButton( icon: Icon(Icons.arrow_back), - onPressed: () => BlocProvider.of(context) - .add(ReturnToInitialState()), + onPressed: () => + BlocProvider.of(context).add(ReturnToInitialState()), ), Expanded( child: Container( - padding: EdgeInsets.symmetric(vertical: 10.0), - child: TextField( - onSubmitted: (text) => - BlocProvider.of(context).add(GetKanji(text)), - decoration: new InputDecoration( - prefixIcon: Icon(Icons.search), - hintText: 'Search for kanji', - fillColor: Colors.white, - filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(100.0)), - ), - ), + child: _KanjiTextField(), ), ), IconButton( @@ -68,3 +70,87 @@ class KanjiViewBar extends StatelessWidget { ); } } + +class _KanjiTextField extends StatefulWidget { + @override + _KanjiTextFieldState createState() => new _KanjiTextFieldState(); +} + +enum TextFieldButton {clear, paste} + +class _KanjiTextFieldState extends State<_KanjiTextField> { + FocusNode _focus = new FocusNode(); + TextEditingController _textController = new TextEditingController(); + TextFieldButton _button = TextFieldButton.paste; + + @override + void initState() { + super.initState(); + _focus.addListener(_onFocusChange); + } + + void _getKanjiSuggestions(String text) => + BlocProvider.of(context).add(GetKanjiSuggestions(text)); + + void updateSuggestions() => _getKanjiSuggestions(_textController.text); + + void _onFocusChange() { + debugPrint('TextField Focus Changed: ${_focus.hasFocus.toString()}'); + + setState(() { + _button = _focus.hasFocus ? TextFieldButton.clear : TextFieldButton.paste; + }); + + if (_focus.hasFocus) + updateSuggestions(); + else + FocusScope.of(context).unfocus(); + } + + void _clearText() { + _textController.text = ''; + updateSuggestions(); + } + + void _pasteText() async { + ClipboardData clipboardData = await Clipboard.getData('text/plain'); + _textController.text = clipboardData.text; + updateSuggestions(); + } + + @override + Widget build(BuildContext context) { + IconButton _clearButton = IconButton( + icon: Icon(Icons.clear), + onPressed: () => _clearText(), + ); + + IconButton _pasteButton = IconButton( + icon: Icon(Icons.content_paste), + onPressed: () => _pasteText(), + ); + + return TextField( + focusNode: _focus, + controller: _textController, + onChanged: (text) => _getKanjiSuggestions(text), + onSubmitted: (text) => + BlocProvider.of(context).add(GetKanji(text)), + decoration: new InputDecoration( + prefixIcon: Icon(Icons.search), + hintText: 'Search for kanji', + fillColor: Colors.white, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(100.0), + ), + contentPadding: EdgeInsets.symmetric(vertical: 10.0), + isDense: false, + suffixIcon: (_button == TextFieldButton.clear) ? _clearButton : _pasteButton, + ), + style: TextStyle( + fontSize: 14.0, + ), + ); + } +} diff --git a/lib/services/kanji_suggestions.dart b/lib/services/kanji_suggestions.dart new file mode 100644 index 0000000..a2adfc2 --- /dev/null +++ b/lib/services/kanji_suggestions.dart @@ -0,0 +1,5 @@ +final kanjiPattern = RegExp(r'[\u3400-\u4DB5\u4E00-\u9FCB\uF900-\uFA6A]'); + +List kanjiSuggestions(String string) { + return kanjiPattern.allMatches(string).map((match) => match.group(0)).toList(); +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 7b108ef..7129eb3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,5 @@ name: jisho_study_tool description: A new Flutter project. - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 environment: @@ -20,45 +9,24 @@ dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. # cupertino_icons: ^0.1.2 unofficial_jisho_api: ^1.1.0 flutter_bloc: ^5.0.1 url_launcher: ^5.5.0 + division: ^0.8.8 dev_dependencies: flutter_test: sdk: flutter - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: + # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: # fonts: # - family: Schyler # fonts: @@ -70,6 +38,3 @@ flutter: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages