diff --git a/assets/images/dbpedia.png b/assets/images/dbpedia.png new file mode 100644 index 0000000..31c4a00 Binary files /dev/null and b/assets/images/dbpedia.png differ 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..7d838d8 --- /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 + 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: () {}), + ], + ), + ); + }, + ); + } + + @override + void initState() { + player.setUrl(widget.audio.uri); + super.initState(); + } +} 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_kana_box.dart b/lib/components/search/search_results_body/parts/kanji_kana_box.dart new file mode 100644 index 0000000..bb90e83 --- /dev/null +++ b/lib/components/search/search_results_body/parts/kanji_kana_box.dart @@ -0,0 +1,86 @@ +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 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.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.word != null; + + @override + Widget build(BuildContext context) { + final String? wordReading = word.reading == null + ? null + : (romajiEnabled && autoTransliterateRomaji + ? transliterateKanaToLatin(word.reading!) + : word.reading!); + + return Container( + margin: margin, + padding: padding, + decoration: BoxDecoration( + color: colors.background, + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.5), + // spreadRadius: 1, + // blurRadius: 0.5, + // offset: const Offset(1, 1), + // ), + // ], + ), + 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: furiganaFontsize ?? + ((kanjiFontsize != null) + ? 0.8 * kanjiFontsize! + : null), + color: wordReading != null ? colors.foreground : Colors.transparent, + ), + ) + : const Text(''), + DefaultTextStyle.merge( + child: hasFurigana + ? Text(word.word!) + : Text(wordReading ?? word.word!), + style: TextStyle(fontSize: kanjiFontsize), + ) + ], + ), + 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..eebedc4 --- /dev/null +++ b/lib/components/search/search_results_body/parts/links.dart @@ -0,0 +1,120 @@ +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), + 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 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 newStringLinks = newLinks.map((l) => l.url).toList(); + + final List icons = [ + ...matches.entries + .map((m) => _patterns[m.key]!(newStringLinks[m.value])) + .toList(), + if (attribution.dbpedia != null) _dbpedia(attribution.dbpedia!) + ]; + + (matches.values.toList()..sort()).reversed.forEach(newLinks.removeAt); + final List otherLinks = + newLinks.map((e) => Text('[${e.text} -> ${e.url}]')).toList(); + + 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..90378a9 --- /dev/null +++ b/lib/components/search/search_results_body/parts/notes.dart @@ -0,0 +1,20 @@ +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) { + return 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..41a4dbb 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; @@ -13,66 +12,28 @@ class OtherForms extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: forms.isNotEmpty ? [ const Text( 'Other Forms', style: TextStyle(fontWeight: FontWeight.bold), ), - Row( - children: forms.map((form) => _KanaBox(form)).toList(), + Wrap( + children: forms + .map( + (form) => KanjiKanaBox( + word: form, + colors: BlocProvider.of(context) + .state + .theme + .menuGreyLight, + ), + ) + .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), - ), - ); - } -} 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..3c40993 --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/antonyms.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import '../../../../../models/themes/theme.dart'; +import 'search_chip.dart'; + +class Antonyms extends StatelessWidget { + final List antonyms; + + const Antonyms({ + Key? key, + required this.antonyms, + }) : 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: antonyms + .map( + (a) => SearchChip( + text: a, + colors: const ColorSet( + foreground: Colors.white, + background: Colors.blue, + ), + ), + ) + .toList(), + ) + ], + ); + } +} 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..629914a --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/english_definitions.dart @@ -0,0 +1,25 @@ +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: englishDefinitions + .map((def) => SearchChip(text: def, colors: colors)) + .toList(), + ); +} 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..8d25663 --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/sense.dart @@ -0,0 +1,90 @@ +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); + + 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 => + _removeAntonyms(List.from(meaning?.supplemental ?? [])); + + bool get hasSupplementalInfo => + sense.info.isNotEmpty || + sense.source.isNotEmpty || + sense.tags.isNotEmpty || + _supplementalWithoutAntonyms.isNotEmpty; + + @override + Widget build(BuildContext context) => BlocBuilder( + builder: (context, state) { + return 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..6bc7566 --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/sentences.dart @@ -0,0 +1,70 @@ +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, + 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) { + return Column( + children: sentences.map((s) => _sentence(s)).toList(), + ); + } +} 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..f3f98e1 --- /dev/null +++ b/lib/components/search/search_results_body/parts/sense/supplemental_info.dart @@ -0,0 +1,45 @@ +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 + 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) _info(sense), + if (sense.info.isNotEmpty) Text(sense.info.join(', ')) + ]; + } + + @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..71645bd 100644 --- a/lib/components/search/search_results_body/parts/senses.dart +++ b/lib/components/search/search_results_body/parts/senses.dart @@ -1,62 +1,47 @@ +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(); + final List senseWidgets = senses + .asMap() + .map( + (k, v) => MapEntry( + v, + extraData?.firstWhereOrNull( + (m) => m.definition == v.englishDefinitions.join('; '), + ), + ), + ) + .entries + .toList() + .asMap() + .entries + .map( + (e) => Sense( + index: e.key, + sense: e.value.key, + meaning: e.value.value, + ), + ) + .toList(); return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, 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), - ), - 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(), - ), - ], - ), - ), - ], - ); - } -} 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..fb7681e 100644 --- a/lib/components/search/search_results_body/search_card.dart +++ b/lib/components/search/search_results_body/search_card.dart @@ -7,8 +7,12 @@ 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/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 +25,117 @@ 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 => + widget.result.senses.map((s) => s.links).expand((l) => l).toList(); + + bool get hasAttribution => + widget.result.attribution.jmdict || + widget.result.attribution.jmnedict || + (widget.result.attribution.dbpedia != null); + + 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: () => '', ), ), + // TODO: find the lowest level in the list. JLPTBadge( - jlptLevel: result.jlpt.isNotEmpty ? result.jlpt[0] : '', + jlptLevel: + widget.result.jlpt.isEmpty ? null : widget.result.jlpt[0], ), - CommonBadge(isCommon: result.isCommon ?? false) + 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()), + ); + + 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) ...[ + const SizedBox(height: 20), + OtherForms(forms: widget.otherForms), + ], + + if (extendedData != null && extendedData.notes.isNotEmpty) ...[ + const SizedBox(height: 20), + Notes(notes: extendedData.notes), + ], + + if (links.isNotEmpty || hasAttribution) ...[ + const SizedBox(height: 20), + 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 2fea4fd..945797e 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