From 2d165021547ef09c7f891c5e9a7b5a07b20bb1fe Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 2 Oct 2024 21:10:22 +0800 Subject: [PATCH] add translator for novel --- lib/network/translator.dart | 75 ++++++++++++++++++ lib/pages/novel_page.dart | 2 +- lib/pages/novel_reading_page.dart | 124 +++++++++++++++++++++++++++--- 3 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 lib/network/translator.dart diff --git a/lib/network/translator.dart b/lib/network/translator.dart new file mode 100644 index 0000000..34a0002 --- /dev/null +++ b/lib/network/translator.dart @@ -0,0 +1,75 @@ +import 'package:pixes/network/app_dio.dart'; + +abstract class Translator { + static Translator? _instance; + + static Translator get instance { + if (_instance == null) { + init(); + } + return _instance!; + } + + static void init() { + _instance = GoogleTranslator(); + } + + /// Translates the given [text] to the given [to] language. + Future translate(String text, String to); +} + +class GoogleTranslator implements Translator { + final Dio _dio = AppDio(); + + String get url => 'https://translate.google.com/translate_a/single'; + + Map buildBody(String text, String to) { + return { + 'q': text, + 'client': 'at', + 'sl': 'auto', + 'tl': to, + 'dt': 't', + 'ie': 'UTF-8', + 'oe': 'UTF-8', + 'dj': '1', + }; + } + + Future translatePart(String part, String to) async { + final response = await _dio.post( + url, + data: buildBody(part, to), + options: Options( + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + ), + ); + var buffer = StringBuffer(); + for(var e in response.data['sentences']) { + buffer.write(e['trans']); + } + return buffer.toString(); + } + + @override + Future translate(String text, String to) async { + final lines = text.split('\n'); + var buffer = StringBuffer(); + var result = ''; + for(int i=0; i 5000) { + result += await translatePart(buffer.toString(), to); + buffer.clear(); + } + buffer.write(line); + buffer.write('\n'); + } + if (buffer.isNotEmpty) { + result += await translatePart(buffer.toString(), to); + } + return result; + } +} \ No newline at end of file diff --git a/lib/pages/novel_page.dart b/lib/pages/novel_page.dart index bd2219c..a114963 100644 --- a/lib/pages/novel_page.dart +++ b/lib/pages/novel_page.dart @@ -646,7 +646,7 @@ class _NovelPageWithIdState extends LoadingState { } class _RelatedNovelsPage extends StatefulWidget { - const _RelatedNovelsPage(this.id, {super.key}); + const _RelatedNovelsPage(this.id); final String id; diff --git a/lib/pages/novel_reading_page.dart b/lib/pages/novel_reading_page.dart index 4b9e38f..7b09d03 100644 --- a/lib/pages/novel_reading_page.dart +++ b/lib/pages/novel_reading_page.dart @@ -7,7 +7,9 @@ import 'package:pixes/components/page_route.dart'; import 'package:pixes/components/title_bar.dart'; import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/image_provider.dart'; +import 'package:pixes/foundation/log.dart'; import 'package:pixes/network/network.dart'; +import 'package:pixes/network/translator.dart'; import 'package:pixes/pages/image_page.dart'; import 'package:pixes/pages/main_page.dart'; import 'package:pixes/utils/ext.dart'; @@ -27,15 +29,36 @@ class _NovelReadingPageState extends LoadingState { bool isShowingSettings = false; + String? translatedContent; + @override void initState() { action = TitleBarAction(MdIcons.tune, "Settings".tl, () { if (!isShowingSettings) { - _NovelReadingSettings.show(context, () { - setState(() {}); - }).then((value) { - isShowingSettings = false; - }); + _NovelReadingSettings.show( + context, + () { + setState(() {}); + }, + TranslationController( + content: data!, + isTranslated: translatedContent != null, + onTranslated: (s) { + setState(() { + translatedContent = s; + }); + }, + revert: () { + setState(() { + translatedContent = null; + }); + }, + ), + ).then( + (value) { + isShowingSettings = false; + }, + ); isShowingSettings = true; } else { Navigator.of(context).pop(); @@ -92,7 +115,7 @@ class _NovelReadingPageState extends LoadingState { ); yield const SizedBox(height: 12.0); - var novelContent = data!.split('\n'); + var novelContent = (translatedContent ?? data!).split('\n'); for (var content in novelContent) { if (content.isEmpty) continue; if (content.startsWith('[uploadedimage:')) { @@ -132,14 +155,38 @@ class _NovelReadingPageState extends LoadingState { } } +class TranslationController { + final String content; + + final bool isTranslated; + + final void Function(String translated) onTranslated; + + final void Function() revert; + + const TranslationController({ + required this.content, + required this.isTranslated, + required this.onTranslated, + required this.revert, + }); +} + class _NovelReadingSettings extends StatefulWidget { - const _NovelReadingSettings(this.callback); + const _NovelReadingSettings(this.callback, this.controller); final void Function() callback; - static Future show(BuildContext context, void Function() callback) { - return Navigator.of(context) - .push(SideBarRoute(_NovelReadingSettings(callback))); + final TranslationController controller; + + static Future show( + BuildContext context, + void Function() callback, + TranslationController controller, + ) { + return Navigator.of(context).push( + SideBarRoute(_NovelReadingSettings(callback, controller)), + ); } @override @@ -256,9 +303,64 @@ class __NovelReadingSettingsState extends State<_NovelReadingSettings> { }), ]), ), - ), + ).paddingBottom(8), + Card( + padding: EdgeInsets.zero, + child: ListTile( + title: Text("Translate Novel".tl), + trailing: widget.controller.isTranslated + ? Button( + onPressed: () { + widget.controller.revert(); + context.pop(); + }, + child: Text("Revert".tl), + ) + : Button( + onPressed: translate, + child: isTranslating + ? const SizedBox( + width: 42, + height: 18, + child: Center( + child: SizedBox.square( + dimension: 18, + child: ProgressRing( + strokeWidth: 2, + ), + ), + ), + ) + : Text("Translate".tl), + ), + ), + ).paddingHorizontal(8).paddingBottom(8), ], ), ); } + + bool isTranslating = false; + + void translate() async { + setState(() { + isTranslating = true; + }); + try { + var translated = await Translator.instance + .translate(widget.controller.content, "zh-CN"); + widget.controller.onTranslated(translated); + if (mounted) { + context.pop(); + } + } catch (e) { + setState(() { + isTranslating = false; + }); + if (mounted) { + context.showToast(message: "Failed to translate".tl); + } + Log.error("Translate", e.toString()); + } + } }