From 28f900c02ea0db449038de26158364218f221925 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Sun, 23 Jan 2022 18:27:00 +0100 Subject: [PATCH] Add more content to search results (#31) * Add lots of new functionality to search results * Fix jlpt level * Make antonyms clickable * Add kanji list * Replace Blocof with BlocBuilder * Make kanji widgets squared * Add missing colons to headers * add romaji under sentences * Fix extensive search not showing * Miscellaneous refactoring --- assets/images/dbpedia.png | Bin 0 -> 28431 bytes lib/components/history/kanji_box.dart | 15 +- lib/components/search/language_selector.dart | 3 +- lib/components/search/search_result_body.dart | 4 +- .../parts/audio_player.dart | 79 ++++++++++ .../search_results_body/parts/badge.dart | 16 +- .../search_results_body/parts/jlpt_badge.dart | 6 +- .../search_results_body/parts/kanji.dart | 73 +++++++++ .../parts/kanji_kana_box.dart | 94 ++++++++++++ .../search_results_body/parts/links.dart | 131 +++++++++++++++++ .../search_results_body/parts/notes.dart | 18 +++ .../parts/other_forms.dart | 91 ++++-------- .../parts/sense/antonyms.dart | 51 +++++++ .../parts/sense/definition_abstract.dart | 20 +++ .../parts/sense/english_definitions.dart | 26 ++++ .../parts/sense/search_chip.dart | 27 ++++ .../parts/sense/sense.dart | 91 ++++++++++++ .../parts/sense/sentences.dart | 66 +++++++++ .../parts/sense/supplemental_info.dart | 47 ++++++ .../search_results_body/parts/senses.dart | 69 +++------ .../parts/wikipedia_attribute.dart | 0 .../search_results_body/search_card.dart | 138 ++++++++++++++---- lib/screens/settings.dart | 11 ++ lib/settings.dart | 3 + pubspec.lock | 65 ++++++++- pubspec.yaml | 5 +- 26 files changed, 985 insertions(+), 164 deletions(-) create mode 100644 assets/images/dbpedia.png create mode 100644 lib/components/search/search_results_body/parts/audio_player.dart create mode 100644 lib/components/search/search_results_body/parts/kanji.dart create mode 100644 lib/components/search/search_results_body/parts/kanji_kana_box.dart create mode 100644 lib/components/search/search_results_body/parts/links.dart create mode 100644 lib/components/search/search_results_body/parts/notes.dart create mode 100644 lib/components/search/search_results_body/parts/sense/antonyms.dart create mode 100644 lib/components/search/search_results_body/parts/sense/definition_abstract.dart create mode 100644 lib/components/search/search_results_body/parts/sense/english_definitions.dart create mode 100644 lib/components/search/search_results_body/parts/sense/search_chip.dart create mode 100644 lib/components/search/search_results_body/parts/sense/sense.dart create mode 100644 lib/components/search/search_results_body/parts/sense/sentences.dart create mode 100644 lib/components/search/search_results_body/parts/sense/supplemental_info.dart delete mode 100644 lib/components/search/search_results_body/parts/wikipedia_attribute.dart diff --git a/assets/images/dbpedia.png b/assets/images/dbpedia.png new file mode 100644 index 0000000000000000000000000000000000000000..31c4a007de50d1d3d57663c5092062632eb114d5 GIT binary patch literal 28431 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 E032FPl>h($ literal 0 HcmV?d00001 diff --git a/lib/components/history/kanji_box.dart b/lib/components/history/kanji_box.dart index 66d8382..4c67f80 100644 --- a/lib/components/history/kanji_box.dart +++ b/lib/components/history/kanji_box.dart @@ -19,18 +19,17 @@ class KanjiBox extends StatelessWidget { final colors = state.theme.menuGreyLight; return Container( padding: const EdgeInsets.all(5), + alignment: Alignment.center, decoration: BoxDecoration( color: colors.background, borderRadius: BorderRadius.circular(10.0), ), - child: Center( - child: FittedBox( - child: Text( - kanji, - style: TextStyle( - color: colors.foreground, - fontSize: 25, - ), + child: FittedBox( + child: Text( + kanji, + style: TextStyle( + color: colors.foreground, + fontSize: 25, ), ), ), diff --git a/lib/components/search/language_selector.dart b/lib/components/search/language_selector.dart index 9b44f30..6aa70a6 100644 --- a/lib/components/search/language_selector.dart +++ b/lib/components/search/language_selector.dart @@ -33,8 +33,9 @@ class _LanguageSelectorState extends State { Widget _languageOption(String language) => Container( + alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), - child: Center(child: Text(language)), + child: Text(language), ); @override diff --git a/lib/components/search/search_result_body.dart b/lib/components/search/search_result_body.dart index d00c770..e85ea7e 100644 --- a/lib/components/search/search_result_body.dart +++ b/lib/components/search/search_result_body.dart @@ -14,7 +14,9 @@ class SearchResultsBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( - children: results.map((result) => SearchResultCard(result: result)).toList(), + children: [ + for (final result in results) SearchResultCard(result: result) + ], ); } } diff --git a/lib/components/search/search_results_body/parts/audio_player.dart b/lib/components/search/search_results_body/parts/audio_player.dart new file mode 100644 index 0000000..df95d14 --- /dev/null +++ b/lib/components/search/search_results_body/parts/audio_player.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:just_audio/just_audio.dart' as ja; +import 'package:unofficial_jisho_api/api.dart'; + +import '../../../../bloc/theme/theme_bloc.dart'; + +class AudioPlayer extends StatefulWidget { + final AudioFile audio; + + const AudioPlayer({ + Key? key, + required this.audio, + }) : super(key: key); + + @override + _AudioPlayerState createState() => _AudioPlayerState(); +} + +class _AudioPlayerState extends State { + final ja.AudioPlayer player = ja.AudioPlayer(); + + double _calculateRelativePlayerPosition(Duration? position) { + if (position != null && player.duration != null) + return position.inMilliseconds / player.duration!.inMilliseconds; + return 0; + } + + bool _isPlaying(ja.PlayerState? state) => state != null && state.playing; + + @override + void initState() { + player.setUrl(widget.audio.uri); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (_, state) { + final ColorSet colors = state.theme.menuGreyLight; + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: colors.background, + ), + child: Row( + children: [ + IconButton( + onPressed: () => player.play().then((_) { + player.stop(); + player.seek(Duration.zero); + }), + iconSize: 30, + icon: StreamBuilder( + stream: player.playerStateStream, + builder: (_, snapshot) => Icon( + _isPlaying(snapshot.data) ? Icons.stop : Icons.play_arrow, + ), + ), + ), + Expanded( + child: StreamBuilder( + stream: player.positionStream, + builder: (_, snapshot) => LinearProgressIndicator( + backgroundColor: colors.foreground, + value: _calculateRelativePlayerPosition(snapshot.data), + ), + ), + ), + + IconButton(icon: const Icon(Icons.volume_up), onPressed: () {}), + ], + ), + ); + }, + ); + } +} diff --git a/lib/components/search/search_results_body/parts/badge.dart b/lib/components/search/search_results_body/parts/badge.dart index 457fc01..fcbc98c 100644 --- a/lib/components/search/search_results_body/parts/badge.dart +++ b/lib/components/search/search_results_body/parts/badge.dart @@ -4,8 +4,11 @@ class Badge extends StatelessWidget { final Widget? child; final Color color; - const Badge({this.child, required this.color, Key? key,}) : super(key: key); - + const Badge({ + Key? key, + this.child, + required this.color, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -18,10 +21,7 @@ class Badge extends StatelessWidget { shape: BoxShape.circle, color: color, ), - child: FittedBox( - child: Center( - child: child, - ), - ), - ); } + child: FittedBox(child: child), + ); + } } diff --git a/lib/components/search/search_results_body/parts/jlpt_badge.dart b/lib/components/search/search_results_body/parts/jlpt_badge.dart index 77e97c5..b15fcdb 100644 --- a/lib/components/search/search_results_body/parts/jlpt_badge.dart +++ b/lib/components/search/search_results_body/parts/jlpt_badge.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import './badge.dart'; class JLPTBadge extends StatelessWidget { - final String jlptLevel; + final String? jlptLevel; const JLPTBadge({ required this.jlptLevel, @@ -10,12 +10,12 @@ class JLPTBadge extends StatelessWidget { }) : super(key: key); String get formattedJlptLevel => - jlptLevel.isNotEmpty ? jlptLevel.substring(5).toUpperCase() : ''; + jlptLevel != null ? jlptLevel!.substring(5).toUpperCase() : ''; @override Widget build(BuildContext context) { return Badge( - color: jlptLevel.isNotEmpty ? Colors.blue : Colors.transparent, + color: jlptLevel != null ? Colors.blue : Colors.transparent, child: Text( formattedJlptLevel, style: const TextStyle(color: Colors.white), diff --git a/lib/components/search/search_results_body/parts/kanji.dart b/lib/components/search/search_results_body/parts/kanji.dart new file mode 100644 index 0000000..876c7e9 --- /dev/null +++ b/lib/components/search/search_results_body/parts/kanji.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +import '../../../../bloc/theme/theme_bloc.dart'; +import '../../../../routing/routes.dart'; + +class KanjiRow extends StatelessWidget { + final List kanji; + final double fontSize; + const KanjiRow({ + Key? key, + required this.kanji, + this.fontSize = 20, + }) : super(key: key); + + Widget _kanjiBox(String kanji) => UnconstrainedBox( + child: IntrinsicHeight( + child: AspectRatio( + aspectRatio: 1, + child: BlocBuilder( + builder: (context, state) { + final colors = state.theme.menuGreyLight; + return Container( + padding: const EdgeInsets.all(10), + alignment: Alignment.center, + decoration: BoxDecoration( + color: colors.background, + borderRadius: BorderRadius.circular(10), + ), + child: FittedBox( + child: Text( + kanji, + style: TextStyle( + color: colors.foreground, + fontSize: fontSize, + ), + ), + ), + ); + }, + ), + ), + ), + ); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Kanji:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 5), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + for (final k in kanji) + InkWell( + onTap: () => Navigator.pushNamed( + context, + Routes.kanjiSearch, + arguments: k, + ), + child: _kanjiBox(k), + ) + ], + ), + ], + ); + } +} diff --git a/lib/components/search/search_results_body/parts/kanji_kana_box.dart b/lib/components/search/search_results_body/parts/kanji_kana_box.dart new file mode 100644 index 0000000..2aa0b43 --- /dev/null +++ b/lib/components/search/search_results_body/parts/kanji_kana_box.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:unofficial_jisho_api/api.dart'; + +import '../../../../models/themes/theme.dart'; +import '../../../../services/romaji_transliteration.dart'; +import '../../../../settings.dart'; + +class KanjiKanaBox extends StatelessWidget { + final JishoJapaneseWord word; + final bool showRomajiBelow; + final ColorSet colors; + final bool autoTransliterateRomaji; + final bool centerFurigana; + final double? furiganaFontsize; + final double? kanjiFontsize; + final EdgeInsets margin; + final EdgeInsets padding; + + const KanjiKanaBox({ + Key? key, + required this.word, + this.showRomajiBelow = false, + this.colors = LightTheme.defaultMenuGreyNormal, + this.autoTransliterateRomaji = true, + this.centerFurigana = true, + this.furiganaFontsize, + this.kanjiFontsize, + this.margin = const EdgeInsets.symmetric( + horizontal: 5.0, + vertical: 5.0, + ), + this.padding = const EdgeInsets.all(5.0), + }) : super(key: key); + + bool get hasFurigana => word.reading != null; + + String get kana => '${word.reading ?? ""}${word.word ?? ""}' + .replaceAll(RegExp(r'\p{Script=Hani}', unicode: true), ''); + + @override + Widget build(BuildContext context) { + final String? wordReading = word.reading == null + ? null + : (romajiEnabled && autoTransliterateRomaji + ? transliterateKanaToLatin(word.reading!) + : word.reading!); + + final fFontsize = furiganaFontsize ?? + ((kanjiFontsize != null) ? 0.8 * kanjiFontsize! : null); + + return Container( + margin: margin, + padding: padding, + color: colors.background, + child: DefaultTextStyle.merge( + child: Column( + crossAxisAlignment: centerFurigana + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + // See header.dart for more details about this logic + hasFurigana + ? Text( + wordReading!, + style: TextStyle( + fontSize: fFontsize, + color: colors.foreground, + ), + ) + : Text( + 'あ', + style: TextStyle( + color: Colors.transparent, + fontSize: fFontsize, + ), + ), + + DefaultTextStyle.merge( + child: hasFurigana + ? Text(word.word!) + : Text(wordReading ?? word.word!), + style: TextStyle(fontSize: kanjiFontsize), + ), + if (romajiEnabled && showRomajiBelow) + Text( + transliterateKanaToLatin(kana), + ) + ], + ), + style: TextStyle(color: colors.foreground), + ), + ); + } +} diff --git a/lib/components/search/search_results_body/parts/links.dart b/lib/components/search/search_results_body/parts/links.dart new file mode 100644 index 0000000..a50bd06 --- /dev/null +++ b/lib/components/search/search_results_body/parts/links.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:unofficial_jisho_api/api.dart'; +import 'package:url_launcher/url_launcher.dart'; + +Future _launch(String url) async { + if (await canLaunch(url)) { + launch(url); + } else { + debugPrint('Could not open url: $url'); + } +} + +final BoxDecoration _iconStyle = BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(10)), + border: Border.all(), +); + +Widget _wiki({ + required String link, + required bool isJapanese, +}) => + Container( + margin: const EdgeInsets.only(right: 10), + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + decoration: _iconStyle, + margin: EdgeInsets.fromLTRB(0, 0, 10, isJapanese ? 12 : 10), + child: IconButton( + onPressed: () => _launch(link), + icon: SvgPicture.asset('assets/images/wikipedia.svg'), + ), + ), + Container( + padding: EdgeInsets.all(isJapanese ? 10 : 8), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all(), + ), + child: Text( + isJapanese ? 'J' : 'E', + style: const TextStyle( + color: Colors.black, + fontFamily: 'serif', + ), + ), + ), + ], + ), + ); + +Widget _dbpedia(String link) => Container( + decoration: _iconStyle, + child: IconButton( + onPressed: () => _launch(link), + icon: Image.asset( + 'assets/images/dbpedia.png', + ), + ), + ); + +final Map _patterns = { + RegExp(r'^Read “.+” on English Wikipedia$'): (l) => + _wiki(link: l, isJapanese: false), + RegExp(r'^Read “.+” on Japanese Wikipedia$'): (l) => + _wiki(link: l, isJapanese: true), + // DBpedia comes through attribution. + // RegExp(r'^Read “.+” on DBpedia$'): _dbpedia, +}; + +class Links extends StatelessWidget { + final List links; + final JishoAttribution attribution; + + const Links({ + Key? key, + required this.links, + required this.attribution, + }) : super(key: key); + + List get _body { + if (links.isEmpty) return []; + + // Copy sense.links so that it doesn't need to be modified. + final List newLinks = List.from(links); + final List newStringLinks = [for (final l in newLinks) l.url]; + + final Map matches = {}; + for (int i = 0; i < newLinks.length; i++) + for (final RegExp p in _patterns.keys) + if (p.hasMatch(newLinks[i].text)) matches[p] = i; + + final List icons = [ + ...[ + for (final match in matches.entries) + _patterns[match.key]!(newStringLinks[match.value]) + ], + if (attribution.dbpedia != null) _dbpedia(attribution.dbpedia!) + ]; + + (matches.values.toList()..sort()).reversed.forEach(newLinks.removeAt); + + final List otherLinks = [ + for (final link in newLinks) ...[ + InkWell( + onTap: () => _launch(link.url), + child: Text( + link.text, + style: const TextStyle(color: Colors.blue), + ), + ) + ] + ]; + + return [ + const Text('Links:', style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 5), + Row(crossAxisAlignment: CrossAxisAlignment.start, children: icons), + const SizedBox(height: 5), + if (otherLinks.isNotEmpty) ...otherLinks, + ]; + } + + @override + Widget build(BuildContext context) => + Column(crossAxisAlignment: CrossAxisAlignment.start, children: _body); +} diff --git a/lib/components/search/search_results_body/parts/notes.dart b/lib/components/search/search_results_body/parts/notes.dart new file mode 100644 index 0000000..d274bfb --- /dev/null +++ b/lib/components/search/search_results_body/parts/notes.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class Notes extends StatelessWidget { + final List notes; + const Notes({Key? key, required this.notes}) : super(key: key); + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Notes:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text(notes.join(', ')), + ], + ); +} diff --git a/lib/components/search/search_results_body/parts/other_forms.dart b/lib/components/search/search_results_body/parts/other_forms.dart index 81c00bc..525b3e4 100644 --- a/lib/components/search/search_results_body/parts/other_forms.dart +++ b/lib/components/search/search_results_body/parts/other_forms.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:unofficial_jisho_api/api.dart'; import '../../../../bloc/theme/theme_bloc.dart'; -import '../../../../services/romaji_transliteration.dart'; -import '../../../../settings.dart'; +import 'kanji_kana_box.dart'; class OtherForms extends StatelessWidget { final List forms; @@ -11,68 +10,28 @@ class OtherForms extends StatelessWidget { const OtherForms({required this.forms, Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - return Column( - children: forms.isNotEmpty - ? [ - const Text( - 'Other Forms', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Row( - children: forms.map((form) => _KanaBox(form)).toList(), - ), - ] - : [], - ); - } -} - -class _KanaBox extends StatelessWidget { - final JishoJapaneseWord word; - - const _KanaBox(this.word); - - bool get hasFurigana => word.word != null; - - @override - Widget build(BuildContext context) { - final _menuColors = - BlocProvider.of(context).state.theme.menuGreyLight; - - final String? wordReading = word.reading == null - ? null - : (romajiEnabled - ? transliterateKanaToLatin(word.reading!) - : word.reading!); - - return Container( - margin: const EdgeInsets.symmetric( - horizontal: 5.0, - vertical: 5.0, - ), - padding: const EdgeInsets.all(5.0), - decoration: BoxDecoration( - color: _menuColors.background, - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 0.5, - offset: const Offset(1, 1), - ), - ], - ), - child: DefaultTextStyle.merge( - child: Column( - children: [ - // See header.dart for more details about this logic - hasFurigana ? Text(wordReading ?? '') : const Text(''), - hasFurigana ? Text(word.word!) : Text(wordReading ?? word.word!), - ], - ), - style: TextStyle(color: _menuColors.foreground), - ), - ); - } + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: forms.isNotEmpty + ? [ + const Text( + 'Other Forms:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Wrap( + children: [ + for (final form in forms) + BlocBuilder( + builder: (context, state) { + return KanjiKanaBox( + word: form, + colors: state.theme.menuGreyLight, + ); + }, + ), + ], + ), + ] + : [], + ); } diff --git a/lib/components/search/search_results_body/parts/sense/antonyms.dart b/lib/components/search/search_results_body/parts/sense/antonyms.dart new file mode 100644 index 0000000..07b8edb --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/antonyms.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import '../../../../../models/themes/theme.dart'; +import '../../../../../routing/routes.dart'; +import 'search_chip.dart'; + +class Antonyms extends StatelessWidget { + final List antonyms; + final ColorSet colors; + + const Antonyms({ + Key? key, + required this.antonyms, + this.colors = const ColorSet( + foreground: Colors.white, + background: Colors.blue, + ), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Antonyms:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 5), + Wrap( + spacing: 5, + runSpacing: 5, + children: [ + for (final antonym in antonyms) + InkWell( + onTap: () => Navigator.pushNamed( + context, + Routes.search, + arguments: antonym, + ), + child: SearchChip( + text: antonym, + colors: colors, + ), + ), + ], + ) + ], + ); + } +} diff --git a/lib/components/search/search_results_body/parts/sense/definition_abstract.dart b/lib/components/search/search_results_body/parts/sense/definition_abstract.dart new file mode 100644 index 0000000..96be835 --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/definition_abstract.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class DefinitionAbstract extends StatelessWidget { + final String text; + final Color? color; + + const DefinitionAbstract({ + Key? key, + required this.text, + this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Text( + text, + style: TextStyle(color: color), + ); + } +} diff --git a/lib/components/search/search_results_body/parts/sense/english_definitions.dart b/lib/components/search/search_results_body/parts/sense/english_definitions.dart new file mode 100644 index 0000000..d5f3d21 --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/english_definitions.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import '../../../../../bloc/theme/theme_bloc.dart'; +import 'search_chip.dart'; + +class EnglishDefinitions extends StatelessWidget { + final List englishDefinitions; + final ColorSet colors; + + const EnglishDefinitions({ + Key? key, + required this.englishDefinitions, + this.colors = LightTheme.defaultMenuGreyNormal, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Wrap( + runSpacing: 10.0, + spacing: 5, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + for (final def in englishDefinitions) + SearchChip(text: def, colors: colors) + ], + ); +} diff --git a/lib/components/search/search_results_body/parts/sense/search_chip.dart b/lib/components/search/search_results_body/parts/sense/search_chip.dart new file mode 100644 index 0000000..637d627 --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/search_chip.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +import '../../../../../models/themes/theme.dart'; + +class SearchChip extends StatelessWidget { + final String text; + final ColorSet colors; + + const SearchChip({ + Key? key, + required this.text, + this.colors = LightTheme.defaultMenuGreyNormal, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: colors.background, + borderRadius: BorderRadius.circular(10.0), + ), + child: Text( + text, + style: TextStyle(color: colors.foreground), + ), + ); +} diff --git a/lib/components/search/search_results_body/parts/sense/sense.dart b/lib/components/search/search_results_body/parts/sense/sense.dart new file mode 100644 index 0000000..f2a36ff --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/sense.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:unofficial_jisho_api/api.dart'; + +import '../../../../../bloc/theme/theme_bloc.dart'; +import 'antonyms.dart'; +import 'definition_abstract.dart'; +import 'english_definitions.dart'; +import 'sentences.dart'; +import 'supplemental_info.dart'; + +class Sense extends StatelessWidget { + final int index; + final JishoWordSense sense; + final PhraseScrapeMeaning? meaning; + + const Sense({ + Key? key, + required this.index, + required this.sense, + this.meaning, + }) : super(key: key); + + // TODO: This assumes that there is only one antonym. However, the + // antonym system is made with the case of multiple antonyms + // in mind. + List _removeAntonyms(List supplementalInfo) { + for (int i = 0; i < supplementalInfo.length; i++) { + if (RegExp(r'^Antonym: .*$').hasMatch(supplementalInfo[i])) { + supplementalInfo.removeAt(i); + break; + } + } + return supplementalInfo; + } + + List? get _supplementalWithoutAntonyms => meaning == null + ? null + : _removeAntonyms(List.from(meaning!.supplemental)); + + bool get hasSupplementalInfo => + sense.info.isNotEmpty || + sense.source.isNotEmpty || + sense.tags.isNotEmpty || + (_supplementalWithoutAntonyms?.isNotEmpty ?? false); + + @override + Widget build(BuildContext context) => BlocBuilder( + builder: (context, state) => Container( + margin: const EdgeInsets.symmetric(vertical: 5), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: state.theme.menuGreyLight.background, + borderRadius: BorderRadius.circular(10.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${index + 1}. ${sense.partsOfSpeech.join(', ')}', + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.left, + ), + EnglishDefinitions( + englishDefinitions: sense.englishDefinitions, + colors: state.theme.menuGreyNormal, + ), + if (hasSupplementalInfo) + SupplementalInfo( + sense: sense, + supplementalInfo: _supplementalWithoutAntonyms, + ), + if (meaning?.definitionAbstract != null) + DefinitionAbstract( + text: meaning!.definitionAbstract!, + color: state.theme.foreground, + ), + if (sense.antonyms.isNotEmpty) Antonyms(antonyms: sense.antonyms), + if (meaning != null && meaning!.sentences.isNotEmpty) + Sentences(sentences: meaning!.sentences) + ] + .map( + (e) => Container( + margin: const EdgeInsets.symmetric(vertical: 5), + child: e, + ), + ) + .toList(), + ), + ), + ); +} diff --git a/lib/components/search/search_results_body/parts/sense/sentences.dart b/lib/components/search/search_results_body/parts/sense/sentences.dart new file mode 100644 index 0000000..9711d9f --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/sentences.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:unofficial_jisho_api/api.dart'; + +import '../../../../../models/themes/theme.dart'; +import '../kanji_kana_box.dart'; + +class Sentences extends StatelessWidget { + final List sentences; + final ColorSet colors; + + const Sentences({ + Key? key, + required this.sentences, + this.colors = LightTheme.defaultMenuGreyNormal, + }) : super(key: key); + + Widget _sentence(PhraseScrapeSentence sentence) => Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: colors.background, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + runSpacing: 10, + children: sentence.pieces + .map( + (p) => JishoJapaneseWord( + word: p.unlifted, + reading: p.lifted, + ), + ) + .map( + (word) => KanjiKanaBox( + word: word, + showRomajiBelow: true, + margin: EdgeInsets.zero, + padding: EdgeInsets.zero, + centerFurigana: false, + autoTransliterateRomaji: false, + kanjiFontsize: 15, + furiganaFontsize: 12, + colors: colors, + ), + ) + .toList(), + ), + Divider( + height: 20, + color: Colors.grey[400], + thickness: 3, + ), + Text( + sentence.english, + style: TextStyle(color: colors.foreground), + ), + ], + ), + ); + + @override + Widget build(BuildContext context) => + Column(children: [for (final s in sentences) _sentence(s)]); +} diff --git a/lib/components/search/search_results_body/parts/sense/supplemental_info.dart b/lib/components/search/search_results_body/parts/sense/supplemental_info.dart new file mode 100644 index 0000000..a52f886 --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/supplemental_info.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:unofficial_jisho_api/api.dart'; + +class SupplementalInfo extends StatelessWidget { + final JishoWordSense sense; + final List? supplementalInfo; + final Color? color; + + const SupplementalInfo({ + Key? key, + required this.sense, + this.supplementalInfo, + this.color, + }) : super(key: key); + + Widget _info(JishoWordSense sense) { + final List restrictions = List.from(sense.restrictions); + if (restrictions.isNotEmpty) + restrictions[0] = 'Only applies to ${restrictions[0]}'; + + final List combinedInfo = sense.tags + sense.info + restrictions; + + return Text( + combinedInfo.join(', '), + style: TextStyle(color: color), + ); + } + + List get _body { + if (supplementalInfo != null) return [Text(supplementalInfo!.join(', '))]; + + return [ + if (sense.source.isNotEmpty) + Text('From ${sense.source[0].language} ${sense.source[0].word}'), + if (sense.tags.isNotEmpty || + sense.restrictions.isNotEmpty || + sense.info.isNotEmpty) + _info(sense), + ]; + } + + @override + Widget build(BuildContext context) => DefaultTextStyle.merge( + child: Column(children: _body), + style: TextStyle(color: color), + ); +} diff --git a/lib/components/search/search_results_body/parts/senses.dart b/lib/components/search/search_results_body/parts/senses.dart index 1bd0ccc..7177948 100644 --- a/lib/components/search/search_results_body/parts/senses.dart +++ b/lib/components/search/search_results_body/parts/senses.dart @@ -1,62 +1,33 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:unofficial_jisho_api/parser.dart'; +import 'package:unofficial_jisho_api/api.dart'; + +import 'sense/sense.dart'; class Senses extends StatelessWidget { final List senses; + final List? extraData; const Senses({ required this.senses, + this.extraData, Key? key, }) : super(key: key); - @override - Widget build(BuildContext context) { - final List senseWidgets = - senses.asMap().entries.map((e) => _Sense(e.key, e.value)).toList(); - - return Column( - children: senseWidgets, - ); - } -} - -class _Sense extends StatelessWidget { - final int index; - final JishoWordSense sense; - - const _Sense(this.index, this.sense); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - children: [ - Text( - '${index + 1}. ', - style: const TextStyle(color: Colors.grey), + List get _senseWidgets => [ + for (int i = 0; i < senses.length; i++) + Sense( + index: i, + sense: senses[i], + meaning: extraData?.firstWhereOrNull( + (m) => m.definition == senses[i].englishDefinitions.join('; '), ), - Text( - sense.partsOfSpeech.join(', '), - style: const TextStyle(fontWeight: FontWeight.bold), - textAlign: TextAlign.left, - ), - ], - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 20), - margin: const EdgeInsets.fromLTRB(0, 5, 0, 15), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: - sense.englishDefinitions.map((def) => Text(def)).toList(), - ), - ], ), - ), - ], - ); - } + ]; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _senseWidgets, + ); } diff --git a/lib/components/search/search_results_body/parts/wikipedia_attribute.dart b/lib/components/search/search_results_body/parts/wikipedia_attribute.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/components/search/search_results_body/search_card.dart b/lib/components/search/search_results_body/search_card.dart index d4c8129..e41ebce 100644 --- a/lib/components/search/search_results_body/search_card.dart +++ b/lib/components/search/search_results_body/search_card.dart @@ -7,8 +7,13 @@ import './parts/jlpt_badge.dart'; import './parts/other_forms.dart'; import './parts/senses.dart'; import './parts/wanikani_badge.dart'; +import '../../../settings.dart'; +import 'parts/audio_player.dart'; +import 'parts/kanji.dart'; +import 'parts/links.dart'; +import 'parts/notes.dart'; -class SearchResultCard extends StatelessWidget { +class SearchResultCard extends StatefulWidget { final JishoResult result; late final JishoJapaneseWord mainWord; late final List otherForms; @@ -21,46 +26,127 @@ class SearchResultCard extends StatelessWidget { super(key: key); @override - Widget build(BuildContext context) { - final backgroundColor = Theme.of(context).scaffoldBackgroundColor; - return ExpansionTile( - collapsedBackgroundColor: backgroundColor, - backgroundColor: backgroundColor, - title: IntrinsicWidth( + _SearchResultCardState createState() => _SearchResultCardState(); +} + +class _SearchResultCardState extends State { + PhrasePageScrapeResultData? extraData; + + Future _scrape(JishoResult result) => + (!(result.japanese[0].word == null && result.japanese[0].reading == null)) + ? scrapeForPhrase( + widget.result.japanese[0].word ?? + widget.result.japanese[0].reading!, + ) + : Future(() => null); + + List get links => + [for (final sense in widget.result.senses) ...sense.links]; + + bool get hasAttribution => + widget.result.attribution.jmdict || + widget.result.attribution.jmnedict || + (widget.result.attribution.dbpedia != null); + + String? get jlptLevel { + if (widget.result.jlpt.isEmpty) return null; + final jlpt = List.from(widget.result.jlpt); + jlpt.sort(); + return jlpt.last; + } + + List get kanji => RegExp(r'(\p{Script=Hani})', unicode: true) + .allMatches( + widget.result.japanese + .map((w) => '${w.word ?? ""}${w.reading ?? ""}') + .join(), + ) + .map((match) => match.group(0)!) + .toSet() + .toList(); + + Widget get _header => IntrinsicWidth( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - JapaneseHeader(word: mainWord), + JapaneseHeader(word: widget.mainWord), Row( children: [ WKBadge( - level: result.tags.firstWhere( + level: widget.result.tags.firstWhere( (tag) => tag.contains('wanikani'), orElse: () => '', ), ), - JLPTBadge( - jlptLevel: result.jlpt.isNotEmpty ? result.jlpt[0] : '', - ), - CommonBadge(isCommon: result.isCommon ?? false) + JLPTBadge(jlptLevel: jlptLevel), + CommonBadge(isCommon: widget.result.isCommon ?? false) ], ) ], ), - ), - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Column( - children: [ - Senses(senses: result.senses), - OtherForms(forms: otherForms), - // Text(result.toJson().toString()), - // Text(result.attribution.toJson().toString()), - // Text(result.japanese.map((e) => e.toJson().toString()).toList().toString()), + ); + + static const _margin = SizedBox(height: 20); + + List _withMargin(Widget w) => [_margin, w]; + + Widget _body({PhrasePageScrapeResultData? extendedData}) => Container( + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (extendedData != null && extendedData.audio.isNotEmpty) ...[ + // TODO: There's usually multiple mimetypes in the data. + // If one mimetype fails, the app should try to use another one. + AudioPlayer(audio: extendedData.audio.first), + const SizedBox(height: 10), ], - ), - ) + Senses( + senses: widget.result.senses, + extraData: extendedData?.meanings, + ), + if (widget.otherForms.isNotEmpty) + ..._withMargin(OtherForms(forms: widget.otherForms)), + if (extendedData != null && extendedData.notes.isNotEmpty) + ..._withMargin(Notes(notes: extendedData.notes)), + if (kanji.isNotEmpty) ..._withMargin(KanjiRow(kanji: kanji)), + if (links.isNotEmpty || hasAttribution) + ..._withMargin( + Links( + links: links, + attribution: widget.result.attribution, + ), + ) + ], + ), + ); + + @override + Widget build(BuildContext context) { + final backgroundColor = Theme.of(context).scaffoldBackgroundColor; + + return ExpansionTile( + collapsedBackgroundColor: backgroundColor, + backgroundColor: backgroundColor, + onExpansionChanged: (b) async { + if (extensiveSearchEnabled && extraData == null) { + final data = await _scrape(widget.result); + setState(() { + extraData = (data != null && data.found) ? data.data : null; + }); + } + }, + title: _header, + children: [ + if (extensiveSearchEnabled && extraData == null) + const Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Center(child: CircularProgressIndicator()), + ) + else if (extraData != null) + _body(extendedData: extraData) + else + _body() ], ); } diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index e69ad44..f702f88 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -60,6 +60,17 @@ class _SettingsViewState extends State { switchValue: romajiEnabled, switchActiveColor: AppTheme.jishoGreen.background, ), + SettingsTile.switchTile( + title: 'Extensive search', + onToggle: (b) { + setState(() => extensiveSearchEnabled = b); + }, + switchValue: extensiveSearchEnabled, + switchActiveColor: AppTheme.jishoGreen.background, + subtitle: + 'Gathers extra data when searching for words, at the expense of having to wait for extra word details', + subtitleMaxLines: 3, + ), ], ), SettingsSection( diff --git a/lib/settings.dart b/lib/settings.dart index c33b52b..9623499 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -5,6 +5,7 @@ final SharedPreferences _prefs = GetIt.instance.get(); const Map _defaults = { 'romajiEnabled': false, + 'extensiveSearch': true, 'darkThemeEnabled': false, 'autoThemeEnabled': false, }; @@ -13,9 +14,11 @@ bool _getSettingOrDefault(String settingName) => _prefs.getBool(settingName) ?? _defaults[settingName]; bool get romajiEnabled => _getSettingOrDefault('romajiEnabled'); +bool get extensiveSearchEnabled => _getSettingOrDefault('extensiveSearch'); bool get darkThemeEnabled => _getSettingOrDefault('darkThemeEnabled'); bool get autoThemeEnabled => _getSettingOrDefault('autoThemeEnabled'); set romajiEnabled(b) => _prefs.setBool('romajiEnabled', b); +set extensiveSearchEnabled(b) => _prefs.setBool('extensiveSearch', b); set darkThemeEnabled(b) => _prefs.setBool('darkThemeEnabled', b); set autoThemeEnabled(b) => _prefs.setBool('autoThemeEnabled', b); diff --git a/pubspec.lock b/pubspec.lock index 71aeb2d..9ab5a3e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -43,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + audio_session: + dependency: transitive + description: + name: audio_session + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.6+1" bloc: dependency: transitive description: @@ -156,7 +163,7 @@ packages: source: hosted version: "4.1.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection url: "https://pub.dartlang.org" @@ -272,6 +279,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -373,6 +387,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.4.0" + just_audio: + dependency: "direct main" + description: + name: just_audio + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.18" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.2" logging: dependency: transitive description: @@ -429,6 +464,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" path_provider: dependency: "direct main" description: @@ -534,6 +583,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.27.3" sembast: dependency: "direct main" description: @@ -763,6 +819,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 19891d4..1dda187 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,14 +7,17 @@ environment: dependencies: animated_size_and_fade: ^3.0.0 + collection: ^1.15.0 confirm_dialog: ^1.0.0 division: ^0.9.0 flutter: sdk: flutter flutter_bloc: ^8.0.0 flutter_slidable: ^1.1.0 + flutter_svg: ^1.0.2 get_it: ^7.2.0 http: ^0.13.4 + just_audio: ^0.9.18 mdi: ^5.0.0-nullsafety.0 path: ^1.8.0 path_provider: ^2.0.2 @@ -41,7 +44,7 @@ flutter: uses-material-design: true assets: - - assets/images/denshi_jisho_background_overlay.png + - assets/images/ - assets/images/logo/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg