diff --git a/lib/player.dart b/lib/player.dart index c1c313b78..7614da9ee 100644 --- a/lib/player.dart +++ b/lib/player.dart @@ -535,11 +535,10 @@ class _VideoPlayerState extends State { // } // } // } - Widget buildDictionaryLoading(String clipboard) { String lookupText; if (globalSelectMode.value) { - lookupText = "Looking up \"$clipboard\"..."; + lookupText = "Looking up『$clipboard』..."; } else { lookupText = "Looking up definition..."; } @@ -672,56 +671,141 @@ class _VideoPlayerState extends State { ); } - Widget buildDictionaryMatch(DictionaryEntry results) { + Widget buildDictionaryMatch(List results) { _subtitleFocusNode.unfocus(); + ValueNotifier selectedIndex = ValueNotifier(0); - return Column( - children: [ - Padding( - padding: EdgeInsets.all(16.0), - child: GestureDetector( - onTap: () { - _clipboard.value = ""; - _currentDictionaryEntry.value = DictionaryEntry( - word: "", - reading: "", - meaning: "", - ); - }, - child: SingleChildScrollView( - child: Container( - padding: EdgeInsets.all(16.0), - color: Colors.grey[800].withOpacity(0.6), + return ValueListenableBuilder( + valueListenable: selectedIndex, + builder: (BuildContext context, int _, Widget widget) { + _currentDictionaryEntry.value = results[selectedIndex.value]; + + return Container( + padding: EdgeInsets.all(16.0), + alignment: Alignment.topCenter, + child: Container( + padding: EdgeInsets.all(16), + margin: EdgeInsets.only(bottom: 84), + color: Colors.grey[800].withOpacity(0.6), + child: GestureDetector( + onHorizontalDragEnd: (details) { + if (details.primaryVelocity == 0) return; + + if (details.primaryVelocity.compareTo(0) == -1) { + if (selectedIndex.value == results.length - 1) { + selectedIndex.value = 0; + } else { + selectedIndex.value += 1; + } + } else { + if (selectedIndex.value == 0) { + selectedIndex.value = results.length - 1; + } else { + selectedIndex.value -= 1; + } + } + }, + onTap: () { + _clipboard.value = ""; + _currentDictionaryEntry.value = DictionaryEntry( + word: "", + reading: "", + meaning: "", + ); + }, child: Column( + mainAxisSize: MainAxisSize.min, children: [ Text( - results.word, + results[selectedIndex.value].word, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 20, ), ), - Text(results.reading), - SelectableText("\n${results.meaning}\n"), + Text(results[selectedIndex.value].reading), + Flexible( + child: SingleChildScrollView( + child: + Text("\n${results[selectedIndex.value].meaning}\n"), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Showing search result ", + style: TextStyle( + fontSize: 11, + ), + textAlign: TextAlign.center, + ), + Text( + "${selectedIndex.value + 1} ", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 11, + ), + textAlign: TextAlign.center, + ), + Text( + "out of ", + style: TextStyle( + fontSize: 11, + ), + textAlign: TextAlign.center, + ), + Text( + "${results.length} ", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 11, + ), + textAlign: TextAlign.center, + ), + Text( + "found for", + style: TextStyle( + fontSize: 11, + ), + textAlign: TextAlign.center, + ), + Text( + "『${results[selectedIndex.value].searchTerm}』", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 11, + ), + textAlign: TextAlign.center, + ), + ], + ) ], ), ), ), - ), - ), - Expanded(child: Container()), - ], - ); + ); + }); } Widget buildDictionary() { return ValueListenableBuilder( valueListenable: _clipboard, builder: (context, clipboard, widget) { + Future> getDictionaryFuture; + if (globalSelectMode.value) { + getDictionaryFuture = getWordDetails(clipboard); + } else { + getDictionaryFuture = getJishoSegmentAndSearch( + _clipboard.value, _currentSubtitle.value.text); + } + return FutureBuilder( - future: getWordDetails(clipboard), - builder: - (BuildContext context, AsyncSnapshot snapshot) { + future: getDictionaryFuture, + builder: (BuildContext context, + AsyncSnapshot> snapshot) { if (_clipboard.value == "&<&>export&<&>") { return buildDictionaryExporting(clipboard); } @@ -738,11 +822,11 @@ class _VideoPlayerState extends State { case ConnectionState.waiting: return buildDictionaryLoading(clipboard); default: - DictionaryEntry entry = snapshot.data; + List entries = snapshot.data; - if (snapshot.hasData) { - _currentDictionaryEntry.value = entry; - return buildDictionaryMatch(entry); + if (snapshot.hasData && snapshot.data.isNotEmpty) { + _currentDictionaryEntry.value = entries.first; + return buildDictionaryMatch(entries); } else { return buildDictionaryNoMatch(clipboard); } diff --git a/lib/util.dart b/lib/util.dart index 68a04bdfa..12c84068b 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -6,10 +6,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_ffmpeg/flutter_ffmpeg.dart'; import 'package:flutter_vlc_player/flutter_vlc_player.dart'; +import 'package:html/dom.dart' as dom; +import 'package:html/parser.dart' as parser; import 'package:http/http.dart' as http; -import 'package:intl/intl.dart'; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart' as intl; import 'package:mecab_dart/mecab_dart.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:subtitle_wrapper_package/data/models/style/subtitle_style.dart'; import 'package:subtitle_wrapper_package/data/models/subtitle.dart'; import 'package:unofficial_jisho_api/api.dart'; import 'package:xml2json/xml2json.dart'; @@ -51,8 +55,8 @@ String timedLineToSRT(Map line, int lineCount) { double duration = double.parse(line["\@dur"]); String text = line["\$"] ?? ""; - text = text.replaceAll("\\n", "\n"); - text = text.replaceAll(""", "\""); + text = text = text.replaceAll("\\n", "\n"); + text = text = text.replaceAll(""", "\""); String startTime = formatTimeString(start); String endTime = formatTimeString(start + duration); @@ -412,7 +416,7 @@ void exportAnkiCard(String deck, String sentence, String answer, String reading, String meaning) { DateTime now = DateTime.now(); String newFileName = - "jidoujisho-" + DateFormat('yyyyMMddTkkmmss').format(now); + "jidoujisho-" + intl.DateFormat('yyyyMMddTkkmmss').format(now); File imageFile = File(previewImageDir); File audioFile = File(previewAudioDir); @@ -442,12 +446,9 @@ class DictionaryEntry { String word; String reading; String meaning; + String searchTerm; - DictionaryEntry({ - this.word, - this.reading, - this.meaning, - }); + DictionaryEntry({this.word, this.reading, this.meaning, this.searchTerm}); } List importCustomDictionary() { @@ -482,16 +483,13 @@ List getAllImportedWords() { return allWords; } -Future getWordDetails(String searchTerm) async { +DictionaryEntry getEntryFromJishoResult(JishoResult result, String searchTerm) { String removeLastNewline(String n) => n = n.substring(0, n.length - 2); bool hasDuplicateReading(String readings, String reading) => readings.contains("$reading; "); - JishoAPIResult results = await searchForPhrase(searchTerm); - JishoResult bestResult = results.data.first; - - List words = bestResult.japanese; - List senses = bestResult.senses; + List words = result.japanese; + List senses = result.senses; String exportTerm = ""; String exportReadings = ""; @@ -522,12 +520,12 @@ Future getWordDetails(String searchTerm) async { if (exportReadings.isNotEmpty) { exportTerm = exportReadings; } else { - exportTerm = bestResult.slug; + exportTerm = result.slug; } } if (exportReadings == "null" || - exportReadings == searchTerm && bestResult.slug == exportReadings) { + exportReadings == searchTerm && result.slug == exportReadings) { exportReadings = ""; } @@ -618,6 +616,45 @@ Future getWordDetails(String searchTerm) async { return dictionaryEntry; } +Future> getWordDetails(String searchTerm) async { + List entries = []; + + List results = (await searchForPhrase(searchTerm)).data; + if (results.isEmpty) { + var client = http.Client(); + http.Response response = + await client.get('https://jisho.org/search/$searchTerm'); + + var document = parser.parse(response.body); + + var breakdown = document.getElementsByClassName("fact grammar-breakdown"); + if (breakdown.isEmpty) { + return []; + } else { + String inflection = breakdown.first.querySelector("a").text; + return getWordDetails(inflection); + } + } + + if (customDictionary.isNotEmpty) { + List onlyFirst = []; + onlyFirst.add(results.first); + results = onlyFirst; + } + + for (JishoResult result in results) { + DictionaryEntry entry = getEntryFromJishoResult(result, searchTerm); + entries.add(entry); + } + + for (DictionaryEntry entry in entries) { + entry.searchTerm = searchTerm; + entry.meaning = getBetterNumberTag(entry.meaning); + } + + return entries; +} + Future getPlayerYouTubeInfo(String webURL) async { var videoID = YoutubePlayer.convertUrlToId(webURL); if (videoID != null) { @@ -831,15 +868,26 @@ class _DeckDropDownState extends State { } } -List> getLinesFromTokens(List tokens) { +List> getLinesFromTokens( + BuildContext context, SubtitleStyle style, List tokens) { List> lines = []; List working = []; String concatenate = ""; + TextPainter textPainter; + + double width = MediaQuery.of(context).size.width; + for (int i = 0; i < tokens.length; i++) { TokenNode token = tokens[i]; + textPainter = TextPainter() + ..text = TextSpan(text: concatenate, style: TextStyle(fontSize: 24)) + ..textDirection = TextDirection.ltr + ..layout(minWidth: 0, maxWidth: double.infinity); + if (token.surface == '␜' || i == tokens.length - 1 || - concatenate.length >= 30) { + textPainter.width >= + width - style.position.left - style.position.right) { List line = []; for (int i = 0; i < working.length; i++) { line.add(working[i]); @@ -848,25 +896,38 @@ List> getLinesFromTokens(List tokens) { lines.add(line); working = []; concatenate = ""; + + working.add(token); + concatenate += token.surface; } else { working.add(token); concatenate += token.surface; - print(token.surface); } } return lines; } -List> getIndexesFromTokens(List tokens) { +List> getIndexesFromTokens( + BuildContext context, SubtitleStyle style, List tokens) { List> lines = []; List working = []; String concatenate = ""; + TextPainter textPainter; + + double width = MediaQuery.of(context).size.width; + for (int i = 0; i < tokens.length; i++) { TokenNode token = tokens[i]; + textPainter = TextPainter() + ..text = TextSpan(text: concatenate, style: TextStyle(fontSize: 24)) + ..textDirection = TextDirection.ltr + ..layout(minWidth: 0, maxWidth: double.infinity); + if (token.surface == '␜' || i == tokens.length - 1 || - concatenate.length >= 30) { + textPainter.width >= + width - style.position.left - style.position.right) { List line = []; for (int i = 0; i < working.length; i++) { line.add(working[i]); @@ -875,12 +936,117 @@ List> getIndexesFromTokens(List tokens) { lines.add(line); working = []; concatenate = ""; + working.add(i); + concatenate += token.surface; } else { working.add(i); concatenate += token.surface; - print(token.surface); } } - return lines; } + +String getBetterNumberTag(String text) { + text = text.replaceAll("50)", "㊿"); + text = text.replaceAll("49)", "㊾"); + text = text.replaceAll("48)", "㊽"); + text = text.replaceAll("47)", "㊼"); + text = text.replaceAll("46)", "㊻"); + text = text.replaceAll("45)", "㊺"); + text = text.replaceAll("44)", "㊹"); + text = text.replaceAll("43)", "㊸"); + text = text.replaceAll("42)", "㊷"); + text = text.replaceAll("41)", "㊶"); + text = text.replaceAll("39)", "㊴"); + text = text.replaceAll("38)", "㊳"); + text = text.replaceAll("37)", "㊲"); + text = text.replaceAll("36)", "㊱"); + text = text.replaceAll("35)", "㉟"); + text = text.replaceAll("34)", "㉞"); + text = text.replaceAll("33)", "㉝"); + text = text.replaceAll("32)", "㉜"); + text = text.replaceAll("31)", "㉛"); + text = text.replaceAll("30)", "㉚"); + text = text.replaceAll("29)", "㉙"); + text = text.replaceAll("28)", "㉘"); + text = text.replaceAll("27)", "㉗"); + text = text.replaceAll("26)", "㉖"); + text = text.replaceAll("25)", "㉕"); + text = text.replaceAll("24)", "㉔"); + text = text.replaceAll("23)", "㉓"); + text = text.replaceAll("22)", "㉒"); + text = text.replaceAll("21)", "㉑"); + text = text.replaceAll("20)", "⑳"); + text = text.replaceAll("19)", "⑲"); + text = text.replaceAll("18)", "⑱"); + text = text.replaceAll("17)", "⑰"); + text = text.replaceAll("16)", "⑯"); + text = text.replaceAll("15)", "⑮"); + text = text.replaceAll("14)", "⑭"); + text = text.replaceAll("13)", "⑬"); + text = text.replaceAll("12)", "⑫"); + text = text.replaceAll("11)", "⑪"); + text = text.replaceAll("10)", "⑩"); + text = text.replaceAll("9)", "⑨"); + text = text.replaceAll("8)", "⑧"); + text = text.replaceAll("7)", "⑦"); + text = text.replaceAll("6)", "⑥"); + text = text.replaceAll("5)", "⑤"); + text = text.replaceAll("4)", "④"); + text = text.replaceAll("3)", "③"); + text = text.replaceAll("2)", "②"); + text = text.replaceAll("1)", "①"); + + return text; +} + +Future> getJishoSegmentAndSearch( + String searchTerm, String fullTerm) async { + var client = http.Client(); + http.Response response = + await client.get('https://jisho.org/search/$fullTerm'); + + var document = parser.parse(response.body); + + List words = []; + + var elements = document.getElementsByClassName("clearfix japanese_word"); + if (elements.isEmpty) { + return getWordDetails(fullTerm); + } + + for (var element in elements) { + var wordWrapper = + element.getElementsByClassName("japanese_word__text_wrapper").first; + + String word; + + var linkElement = wordWrapper.getElementsByTagName("a"); + if (linkElement.isNotEmpty) { + word = linkElement.first.attributes["data-word"]; + } else { + word = wordWrapper.text; + } + + words.add(word.trim()); + } + + List indexTape = []; + for (int i = 0; i < words.length; i++) { + for (int j = 0; j < words[i].length; j++) { + indexTape.add(i); + } + } + + int startIndex = fullTerm.length - searchTerm.length; + + print("WEB SCRAPING TEXT SEGMENTATION"); + print("WORDS: $words"); + print("INDEX TAPE: $indexTape"); + print("START INDEX: $startIndex"); + print("INDEX OF ENTRY WORD: ${indexTape[startIndex]}"); + + print("ENTRY WORD: ${words[indexTape[startIndex]]}"); + + return getWordDetails(words[indexTape[startIndex]]); +} diff --git a/subtitle_wrapper_package/lib/subtitle_text_view.dart b/subtitle_wrapper_package/lib/subtitle_text_view.dart index ede697c18..020239a32 100644 --- a/subtitle_wrapper_package/lib/subtitle_text_view.dart +++ b/subtitle_wrapper_package/lib/subtitle_text_view.dart @@ -119,8 +119,10 @@ class SubtitleTextView extends StatelessWidget { var pretokens = state.subtitle.text.replaceAll('\n', '␜'); var tokens = mecabTagger.parse(pretokens.replaceAll(' ', '␝')); - List> lines = getLinesFromTokens(tokens); - List> indexes = getIndexesFromTokens(tokens); + List> lines = + getLinesFromTokens(context, subtitleStyle, tokens); + List> indexes = + getIndexesFromTokens(context, subtitleStyle, tokens); print("MECAB TOKENS"); print(lines); @@ -129,27 +131,26 @@ class SubtitleTextView extends StatelessWidget { children: [ subtitleStyle.hasBorder ? Center( - child: SingleChildScrollView( - child: ListView.builder( - shrinkWrap: true, - itemCount: lines.length, - itemBuilder: - (BuildContext context, int lineIndex) { - List line = lines[lineIndex]; - List textWidgets = []; - - for (int i = 0; i < line.length; i++) { - TokenNode token = line[i]; - textWidgets.add(getOutlineText(token)); - } - - return Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: textWidgets, - ); - }, - ), + child: ListView.builder( + shrinkWrap: true, + itemCount: lines.length, + physics: BouncingScrollPhysics(), + itemBuilder: + (BuildContext context, int lineIndex) { + List line = lines[lineIndex]; + List textWidgets = []; + + for (int i = 0; i < line.length; i++) { + TokenNode token = line[i]; + textWidgets.add(getOutlineText(token)); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: textWidgets, + ); + }, ), ) : Container( @@ -157,130 +158,33 @@ class SubtitleTextView extends StatelessWidget { ), Center( child: Center( - child: SingleChildScrollView( - child: ListView.builder( - shrinkWrap: true, - itemCount: lines.length, - itemBuilder: - (BuildContext context, int lineIndex) { - List line = lines[lineIndex]; - List indexList = indexes[lineIndex]; - List textWidgets = []; - - for (int i = 0; i < line.length; i++) { - TokenNode token = line[i]; - int index = indexList[i]; - textWidgets - .add(getText(tokens, token, index)); - } - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: textWidgets, - ); - }, - ), + child: ListView.builder( + shrinkWrap: true, + itemCount: lines.length, + physics: BouncingScrollPhysics(), + itemBuilder: (BuildContext context, int lineIndex) { + List line = lines[lineIndex]; + List indexList = indexes[lineIndex]; + List textWidgets = []; + + for (int i = 0; i < line.length; i++) { + TokenNode token = line[i]; + int index = indexList[i]; + textWidgets.add(getText(tokens, token, index)); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: textWidgets, + ); + }, ), ), ), - // Center( - // child: SelectableText( - // state.subtitle.text, - // key: ViewKeys.SUBTITLE_TEXT_CONTENT, - // textAlign: TextAlign.center, - // style: TextStyle( - // fontSize: subtitleStyle.fontSize, - // color: subtitleStyle.textColor, - // ), - // toolbarOptions: ToolbarOptions( - // copy: true, cut: false, selectAll: false, paste: false), - // ), - // ), ], ), ); - - // return Container( - // child: Stack( - // children: [ - // subtitleStyle.hasBorder - // ? Center( - // child: SingleChildScrollView( - // scrollDirection: Axis.horizontal, - // child: Wrap( - // crossAxisAlignment: WrapCrossAlignment.end, - // children: List.generate( - // tokens.length - 1, (index) { - // TokenNode token = tokens[index]; - - // return Text( - // token.surface.replaceAll('␝', ' '), - // style: TextStyle( - // fontSize: subtitleStyle.fontSize, - // foreground: Paint() - // ..style = - // subtitleStyle.borderStyle.style - // ..strokeWidth = subtitleStyle - // .borderStyle.strokeWidth - // ..color = - // Colors.black.withOpacity(0.75), - // ), - // ); - // }), - // ), - // ), - // ) - // : Container( - // child: null, - // ), - // Center( - // child: SingleChildScrollView( - // scrollDirection: Axis.horizontal, - // child: Wrap( - // crossAxisAlignment: WrapCrossAlignment.end, - // children: List.generate(tokens.length - 1, - // (index) { - // TokenNode token = tokens[index]; - - // return GestureDetector( - // onTap: () { - // String allText = ""; - // for (int i = index; i < tokens.length; i++) { - // allText += - // tokens[i].surface.replaceAll('␝', ' '); - // } - - // Clipboard.setData( - // ClipboardData(text: allText), - // ); - // }, - // child: Text( - // token.surface.replaceAll('␝', ' '), - // style: TextStyle( - // fontSize: subtitleStyle.fontSize, - // ), - // ), - // ); - // }), - // ), - // ), - // ), - // // Center( - // // child: SelectableText( - // // state.subtitle.text, - // // key: ViewKeys.SUBTITLE_TEXT_CONTENT, - // // textAlign: TextAlign.center, - // // style: TextStyle( - // // fontSize: subtitleStyle.fontSize, - // // color: subtitleStyle.textColor, - // // ), - // // toolbarOptions: ToolbarOptions( - // // copy: true, cut: false, selectAll: false, paste: false), - // // ), - // // ), - // ], - // ), - // ); } }, );