Merge pull request #5 from h7x4ABk3g/development

Make project base
pull/6/head
Oystein Kristoffer Tveit 2020-08-24 23:51:22 +02:00 committed by GitHub
commit 5eaf220683
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1438 additions and 333 deletions

2
.gitignore vendored
View File

@ -35,3 +35,5 @@ lib/generated_plugin_registrant.dart
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
pubspec.lock

View File

@ -1,16 +1,20 @@
# jisho_study_tool
# Jisho Study tool
A new Flutter project.
A japanese dictionary with features for making studying the language easier.
## Getting Started
## Search
This project is a starting point for a Flutter application.
Standard search using Jishos own API. This returns standard Jisho search results, including what you'd find if you searched for something through the standard jisho search bar without any #modifiers
A few resources to get you started if this is your first Flutter project:
## Kanji Search
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
Standard kanji search using the #kanji modifier. This will give you detailed information about things like drawing order, radicals, different kinds of ranks and statistics and some onyomi and kunyomi example words.
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## Upcoming Features
* Different kinds of kanji input like radicals and grade based lists
* Favorites, history and custom lists
* Anki export
* Memo cards
* Cloud sync
* Dark theme

View File

@ -0,0 +1,18 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
part 'history_event.dart';
part 'history_state.dart';
class HistoryBloc extends Bloc<HistoryEvent, HistoryState> {
HistoryBloc() : super(HistoryInitial());
@override
Stream<HistoryState> mapEventToState(
HistoryEvent event,
) async* {
// TODO: implement mapEventToState
}
}

View File

@ -0,0 +1,4 @@
part of 'history_bloc.dart';
@immutable
abstract class HistoryEvent {}

View File

@ -0,0 +1,6 @@
part of 'history_bloc.dart';
@immutable
abstract class HistoryState {}
class HistoryInitial extends HistoryState {}

View File

@ -0,0 +1,41 @@
import 'dart:async';
import './kanji_event.dart';
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';
class KanjiBloc extends Bloc<KanjiEvent, KanjiState> {
KanjiBloc() : super(KanjiSearchInitial());
@override
Stream<KanjiState> mapEventToState(
KanjiEvent event,
) async* {
if (event is GetKanji) {
yield KanjiSearchLoading();
try {
final _kanji = await fetchKanji(event.kanjiSearchString);
if (_kanji.found) yield KanjiSearchFinished(kanji: _kanji);
else yield KanjiSearchError('Something went wrong');
} 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();
}
}
}

View File

@ -0,0 +1,17 @@
abstract class KanjiEvent {
const KanjiEvent();
}
class GetKanjiSuggestions extends KanjiEvent {
final String searchString;
const GetKanjiSuggestions(this.searchString);
}
class GetKanji extends KanjiEvent {
final String kanjiSearchString;
const GetKanji(this.kanjiSearchString);
}
class ReturnToInitialState extends KanjiEvent {
const ReturnToInitialState();
}

View File

@ -0,0 +1,34 @@
import 'package:unofficial_jisho_api/api.dart';
abstract class KanjiState {
const KanjiState();
}
class KanjiSearchInitial extends KanjiState {
const KanjiSearchInitial();
}
class KanjiSearchInput extends KanjiState {
final List<String> kanjiSuggestions;
const KanjiSearchInput(this.kanjiSuggestions);
}
class KanjiSearchLoading extends KanjiState {
const KanjiSearchLoading();
}
class KanjiSearchFinished extends KanjiState {
final KanjiResult kanji;
final bool starred;
const KanjiSearchFinished({
this.kanji,
this.starred = false,
});
}
class KanjiSearchError extends KanjiState {
final String message;
const KanjiSearchError(this.message);
}

View File

@ -0,0 +1,34 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:jisho_study_tool/services/jisho_search.dart';
import 'package:unofficial_jisho_api/parser.dart';
part 'search_event.dart';
part 'search_state.dart';
class SearchBloc extends Bloc<SearchEvent, SearchState> {
SearchBloc() : super(SearchInitial());
@override
Stream<SearchState> mapEventToState(
SearchEvent event,
) async* {
if (event is GetSearchResults) {
yield SearchLoading();
try {
final _searchResults = await fetchJishoResults(event.searchString);
if (_searchResults.meta.status == 200) yield SearchFinished(_searchResults.data);
} on Exception {
yield SearchError('Something went wrong');
}
} else if (event is ReturnToInitialState) {
yield SearchInitial();
}
}
}

View File

@ -0,0 +1,15 @@
part of 'search_bloc.dart';
@immutable
abstract class SearchEvent {
const SearchEvent();
}
class GetSearchResults extends SearchEvent {
final String searchString;
const GetSearchResults(this.searchString);
}
class ReturnToInitialState extends SearchEvent {
const ReturnToInitialState();
}

View File

@ -0,0 +1,26 @@
part of 'search_bloc.dart';
@immutable
abstract class SearchState {
const SearchState();
}
class SearchInitial extends SearchState {
const SearchInitial();
}
class SearchLoading extends SearchState {
const SearchLoading();
}
class SearchFinished extends SearchState {
final List<JishoResult> results;
const SearchFinished(this.results);
}
class SearchError extends SearchState {
final String message;
const SearchError(this.message);
}

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:unofficial_jisho_api/api.dart' as jisho;
import 'parts/grade.dart';
import 'parts/header.dart';
import 'parts/jlpt_level.dart';
import 'parts/meaning.dart';
import 'parts/radical.dart';
import 'parts/rank.dart';
import 'parts/stroke_order_gif.dart';
import 'parts/onyomi.dart';
import 'parts/kunyomi.dart';
import 'parts/examples.dart';
class KanjiResultCard extends StatelessWidget {
final jisho.KanjiResult _result;
@override
Widget build(BuildContext context) {
return ListView(
children: [
Container(
margin: EdgeInsets.fromLTRB(20.0, 20.0, 20.0, 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Flexible(
flex: 1,
fit: FlexFit.tight,
child: Center(child: SizedBox()),
),
Flexible(
flex: 1,
fit: FlexFit.tight,
child: Center(child: Header(_result.query)),
),
Flexible(
flex: 1,
fit: FlexFit.tight,
child: Center(
child: Radical(_result.radical),
),
),
],
),
),
Meaning(_result.meaning),
_result.onyomi.length != 0 ? Onyomi(_result.onyomi) : SizedBox.shrink(),
_result.kunyomi.length != 0 ? Kunyomi(_result.kunyomi) : SizedBox.shrink(),
IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
StrokeOrderGif(_result.strokeOrderGifUri),
Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text("JLPT: ", style: TextStyle(fontSize: 20.0)),
JlptLevel(_result.jlptLevel ?? ""),
],
),
Row(
children: [
Text("Grade: ", style: TextStyle(fontSize: 20.0)),
Grade(_result.taughtIn ?? ""),
],
),
Row(
children: [
Text("Rank: ", style: TextStyle(fontSize: 20.0)),
Rank(_result.newspaperFrequencyRank ?? -1),
],
),
],
),
),
],
),
),
Examples(_result.onyomiExamples, _result.kunyomiExamples),
],
);
}
KanjiResultCard(this._result);
}

View File

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:unofficial_jisho_api/api.dart';
class Examples extends StatelessWidget {
final List<YomiExample> _onyomiExamples;
final List<YomiExample> _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,
),
),
)
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class Grade extends StatelessWidget {
final String _grade;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Text(
_grade,
style: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
),
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
);
}
Grade(this._grade);
}

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class Header extends StatelessWidget {
final String _kanji;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0), color: Colors.blue),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
_kanji,
style: TextStyle(fontSize: 80.0, color: Colors.white),
),
),
);
}
Header(this._kanji);
}

View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class JlptLevel extends StatelessWidget {
final String _jlptLevel;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Text(
_jlptLevel,
style: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
);
}
JlptLevel(this._jlptLevel);
}

View File

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
class Kunyomi extends StatelessWidget {
final List<String> _kunyomi;
List<_KunyomiCard> _kunyomiCards;
bool _expandable;
Kunyomi(this._kunyomi) {
_kunyomiCards = _kunyomi.map((kunyomi) => _KunyomiCard(kunyomi)).toList();
_expandable = (_kunyomi.length > 6);
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 5.0,
),
alignment: Alignment.centerLeft,
child: _KunyomiWrapper(context),
);
}
Widget _KunyomiWrapper(BuildContext context) {
if (_expandable) {
return ExpansionTile(
initiallyExpanded: false,
title: Center(child: _KunyomiCard('Kunyomi')),
children: [
SizedBox(
height: 20.0,
),
Wrap(
runSpacing: 10.0,
children: _kunyomiCards,
),
SizedBox(
height: 25.0,
),
],
);
} else {
return Wrap(
runSpacing: 10.0,
children: _kunyomiCards,
);
}
}
}
class _KunyomiCard extends StatelessWidget {
final String _kunyomi;
const _KunyomiCard(this._kunyomi);
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 10.0),
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 10.0,
),
child: Text(
_kunyomi,
style: TextStyle(
fontSize: 20.0,
color: Colors.white,
),
),
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(10.0),
),
);
}
}

View File

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
class Meaning extends StatelessWidget {
List<String> _meanings;
List<_MeaningCard> _meaningCards;
bool _expandable;
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 5.0,
),
alignment: Alignment.centerLeft,
child: _MeaningWrapper(context),
);
}
Widget _MeaningWrapper(BuildContext context) {
if (_expandable) {
return ExpansionTile(
initiallyExpanded: false,
title: Center(child: _MeaningCard('Meanings')),
children: [
SizedBox(
height: 20.0,
),
Wrap(
runSpacing: 10.0,
children: _meaningCards,
),
SizedBox(
height: 25.0,
),
],
);
} else {
return Wrap(
runSpacing: 10.0,
children: _meaningCards,
);
}
}
Meaning(_meaning) {
this._meanings = _meaning.split(', ');
this._meaningCards =
_meanings.map((meaning) => _MeaningCard(meaning)).toList();
this._expandable = (this._meanings.length > 6);
}
}
class _MeaningCard extends StatelessWidget {
final String _meaning;
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 10.0),
padding: EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 10.0,
),
child: Text(
_meaning,
style: TextStyle(
fontSize: 20.0,
color: Colors.white,
),
),
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(10.0),
),
);
}
_MeaningCard(this._meaning);
}

View File

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
class Onyomi extends StatelessWidget {
final List<String> _onyomi;
List<_OnyomiCard> _onyomiCards;
bool _expandable;
Onyomi(this._onyomi) {
_onyomiCards = _onyomi.map((onyomi) => _OnyomiCard(onyomi)).toList();
_expandable = (_onyomi.length > 6);
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 5.0,
),
alignment: Alignment.centerLeft,
child: _OnyomiWrapper(context),
);
}
Widget _OnyomiWrapper(BuildContext context) {
if (_expandable) {
return ExpansionTile(
initiallyExpanded: false,
title: Center(child: _OnyomiCard('Onyomi')),
children: [
SizedBox(
height: 20.0,
),
Wrap(
runSpacing: 10.0,
children: _onyomiCards,
),
SizedBox(
height: 25.0,
),
],
);
} else {
return Wrap(
runSpacing: 10.0,
children: _onyomiCards,
);
}
}
}
class _OnyomiCard extends StatelessWidget {
final String _onyomi;
const _OnyomiCard(this._onyomi);
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 10.0),
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 10.0,
),
child: Text(
_onyomi,
style: TextStyle(
fontSize: 20.0,
color: Colors.white,
),
),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(10.0),
),
);
}
}

View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:unofficial_jisho_api/api.dart' as jisho;
class Radical extends StatelessWidget {
final jisho.Radical _radical;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Text(
_radical.symbol,
style: TextStyle(
color: Colors.white,
fontSize: 40.0,
),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
);
}
Radical(this._radical);
}

View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class Rank extends StatelessWidget {
final int _rank;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Text(
'${_rank.toString()} / 2500',
style: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
color: Colors.blue,
),
);
}
Rank(this._rank);
}

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class StrokeOrderGif extends StatelessWidget {
final String _uri;
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(vertical: 20.0),
padding: EdgeInsets.all(5.0),
child: ClipRRect(
child: Image.network(_uri),
borderRadius: BorderRadius.circular(10.0),
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(15.0),
),
);
}
StrokeOrderGif(this._uri);
}

View File

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
class KanjiGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 3,
);
}
}

View File

@ -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<String> _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<KanjiBloc>(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),
),
),
),
),
);
}
}

View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class LoadingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
}
}

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:unofficial_jisho_api/api.dart';
class JapaneseHeader extends StatelessWidget {
final JishoJapaneseWord _word;
const JapaneseHeader(this._word);
@override
Widget build(BuildContext context) {
final hasFurigana = (_word.word != null);
return Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 10.0),
child: Column(
children: [
(hasFurigana) ? Text(_word.reading) : Text(''),
(hasFurigana) ? Text(_word.word) : Text(_word.reading),
],
),
);
}
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:unofficial_jisho_api/api.dart';
class OtherForms extends StatelessWidget {
final List<JishoJapaneseWord> _otherForms;
OtherForms(this._otherForms);
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text(
'Other Forms',
style: TextStyle(fontWeight: FontWeight.bold),
),
Row(
children: _otherForms.map((form) => _KanaBox(form)).toList(),
),
],
),
);
}
}
class _KanaBox extends StatelessWidget {
final JishoJapaneseWord _word;
const _KanaBox(this._word);
@override
Widget build(BuildContext context) {
final hasFurigana = (_word.word != null);
return Container(
child: Column(
children: [
(hasFurigana) ? Text(_word.reading) : Text(''),
(hasFurigana) ? Text(_word.word) : Text(_word.reading),
],
),
margin: EdgeInsets.symmetric(
horizontal: 5.0,
vertical: 5.0,
),
padding: EdgeInsets.all(5.0),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 0.5,
offset: Offset(1, 1),
),
],
),
);
}
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:unofficial_jisho_api/parser.dart';
class Senses extends StatelessWidget {
final List<JishoWordSense> _senses;
const Senses(this._senses);
@override
Widget build(BuildContext context) {
final List<Widget> _senseWidgets =
_senses.map((sense) => _Sense(sense)).toList();
return Container(
child: Column(
children: _senseWidgets,
));
}
}
class _Sense extends StatelessWidget {
final JishoWordSense _sense;
const _Sense(this._sense);
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text(
_sense.parts_of_speech.join(', '),
style: TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.left,
),
Column(
children:
_sense.english_definitions.map((def) => Text(def)).toList(),
)
],
),
);
}
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:unofficial_jisho_api/api.dart';
import 'parts/header.dart';
import 'parts/senses.dart';
import 'parts/other_forms.dart';
class SearchResultCard extends StatelessWidget {
final JishoResult _result;
JishoJapaneseWord _mainWord;
List<JishoJapaneseWord> _otherForms;
SearchResultCard(this._result) {
this._mainWord = _result.japanese[0];
this._otherForms = _result.japanese.sublist(1);
}
@override
Widget build(BuildContext context) {
return ExpansionTile(
title: JapaneseHeader(_mainWord),
children: [
Senses(_result.senses),
OtherForms(_otherForms),
],
);
}
}

View File

@ -1,4 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jisho_study_tool/bloc/kanji/kanji_bloc.dart';
import 'package:jisho_study_tool/screens/kanji_search.dart';
import 'package:jisho_study_tool/screens/history.dart';
import 'package:jisho_study_tool/screens/search.dart';
import 'bloc/search/search_bloc.dart';
void main() => runApp(MyApp());
@ -7,11 +14,17 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: 'Jisho Study Tool',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Home(),
home: MultiBlocProvider(
providers: [
BlocProvider(create: (context) => SearchBloc()),
BlocProvider(create: (context) => KanjiBloc()),
],
child: Home(),
),
);
}
}
@ -19,55 +32,88 @@ class MyApp extends StatelessWidget {
class Home extends StatefulWidget {
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int _selectedPage = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Jisho Study Tool')
),
body: Container(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedPage,
onTap: (int index) {
setState(() {
_selectedPage = index;
});
},
items: [
BottomNavigationBarItem(
title: Text('Search'),
icon: Icon(Icons.search)
),
BottomNavigationBarItem(
title: Text('Kanji'),
icon: Text(
'',
style: TextStyle(
fontSize: 18
),
)
),
BottomNavigationBarItem(
title: Text('Memorize'),
icon: Icon(Icons.book)
),
BottomNavigationBarItem(
title: Text('Settings'),
icon: Icon(Icons.settings)
),
],
showSelectedLabels: false,
showUnselectedLabels: false,
unselectedItemColor: Colors.blue,
selectedItemColor: Colors.green,
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: pages[_selectedPage].titleBar,
centerTitle: true,
),
body: pages[_selectedPage].content,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedPage,
onTap: (int index) {
setState(() {
_selectedPage = index;
});
},
items: navBar,
showSelectedLabels: false,
showUnselectedLabels: false,
unselectedItemColor: Colors.blue,
selectedItemColor: Colors.green,
),
);
}
}
}
final List<BottomNavigationBarItem> navBar = [
BottomNavigationBarItem(
title: Text('Search'),
icon: Icon(Icons.search),
),
BottomNavigationBarItem(
title: Text('Kanji'),
icon: Text(
'',
style: TextStyle(fontSize: 18),
),
),
BottomNavigationBarItem(
title: Text('History'),
icon: Icon(Icons.bookmark),
),
BottomNavigationBarItem(
title: Text('Memorize'),
icon: Icon(Icons.local_offer),
),
BottomNavigationBarItem(
title: Text('Settings'),
icon: Icon(Icons.settings),
),
];
class Page {
Widget content;
Widget titleBar;
Page({
this.content,
this.titleBar,
});
}
final List<Page> pages = [
Page(content: SearchView(), titleBar: Text('Search')),
Page(
content: KanjiView(),
titleBar: KanjiViewBar(),
),
Page(
content: HistoryView(),
titleBar: Text("History"),
),
Page(
content: Container(),
titleBar: Text("Memorization"),
),
Page(
content: Container(),
titleBar: Text("Settings"),
),
];

10
lib/screens/history.dart Normal file
View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class HistoryView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) => ListTile(),
);
}
}

View File

@ -0,0 +1,153 @@
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 BlocListener<KanjiBloc, KanjiState>(
listener: (context, state) {
if (state is KanjiSearchInitial) {
FocusScope.of(context).unfocus();
} else if (state is KanjiSearchLoading) {
FocusScope.of(context).unfocus();
}
},
child: BlocBuilder<KanjiBloc, KanjiState>(
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<KanjiBloc>(context)
.add(ReturnToInitialState());
return false;
});
throw 'No such event found';
},
),
);
}
}
class KanjiViewBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () =>
BlocProvider.of<KanjiBloc>(context).add(ReturnToInitialState()),
),
Expanded(
child: Container(
child: _KanjiTextField(),
),
),
IconButton(
icon: Icon(Icons.star_border),
onPressed: null,
),
IconButton(
icon: Icon(Icons.add),
onPressed: null,
),
],
),
);
}
}
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<KanjiBloc>(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<KanjiBloc>(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,
),
);
}
}

View File

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jisho_study_tool/bloc/search/search_bloc.dart';
import 'package:jisho_study_tool/components/loading.dart';
import 'package:jisho_study_tool/components/search/search_card.dart';
class SearchView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocListener<SearchBloc, SearchState>(
listener: (context, state) {},
child: BlocBuilder<SearchBloc, SearchState>(
builder: (context, state) {
if (state is SearchInitial)
return _InitialView();
else if (state is SearchLoading)
return LoadingScreen();
else if (state is SearchFinished) {
return WillPopScope(
child: ListView(
children: state.results
.map((result) => SearchResultCard(result))
.toList(),
),
onWillPop: () async {
BlocProvider.of<SearchBloc>(context)
.add(ReturnToInitialState());
print('Popped');
return false;
},
);
}
throw 'No such event found';
},
));
}
}
class _InitialView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
SearchBar(),
]);
}
}
class _LanguageOption extends StatelessWidget {
final String _language;
final Color _color;
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: Center(child: Text(_language)),
decoration: BoxDecoration(
border: Border.all(
color: Colors.black,
width: 1.0,
),
color: _color),
),
);
}
_LanguageOption(this._language, this._color);
}
class SearchBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
children: [
TextField(
onSubmitted: (text) => BlocProvider.of<SearchBloc>(context)
.add(GetSearchResults(text)),
controller: TextEditingController(),
decoration: InputDecoration(
labelText: 'Search',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
),
SizedBox(
height: 10.0,
),
Row(
children: [
_LanguageOption('Auto', Colors.white),
_LanguageOption('English', Colors.white),
_LanguageOption('Japanese', Colors.blue),
],
),
],
),
);
}
}

View File

@ -0,0 +1,5 @@
import 'package:unofficial_jisho_api/api.dart' as jisho;
Future<jisho.JishoAPIResult> fetchJishoResults(searchTerm) async {
return await jisho.searchForPhrase(searchTerm);
}

View File

@ -0,0 +1,23 @@
import 'package:unofficial_jisho_api/api.dart' as jisho;
String _convertGrade(String grade) {
const _conversionTable = {
"grade 1": "小1",
"grade 2": "小2",
"grade 3": "小3",
"grade 4": "小4",
"grade 5": "小5",
"grade 6": "小6",
"junior high": ""
};
print('conversion run: $grade -> ${_conversionTable[grade]}');
return _conversionTable[grade];
}
Future<jisho.KanjiResult> fetchKanji(String kanji) async {
final result = await jisho.searchForKanji(kanji);
result.taughtIn = _convertGrade(result.taughtIn);
return result;
}

View File

@ -0,0 +1,5 @@
final kanjiPattern = RegExp(r'[\u3400-\u4DB5\u4E00-\u9FCB\uF900-\uFA6A]');
List<String> kanjiSuggestions(String string) {
return kanjiPattern.allMatches(string).map((match) => match.group(0)).toList();
}

View File

@ -1,237 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.2"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.11"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0+3"
html_unescape:
dependency: transitive
description:
name: html_unescape
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1+3"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.1"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.4"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.4"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0+1"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.5"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.11"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
unofficial_jisho_api:
dependency: "direct main"
description:
name: unofficial_jisho_api
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.5.0"
sdks:
dart: ">=2.7.0 <3.0.0"

View File

@ -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,43 +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.0.2
# cupertino_icons: ^0.1.2
unofficial_jisho_api: ^1.1.0
flutter_bloc: ^6.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:
@ -68,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