add translator for novel

This commit is contained in:
nyne
2024-10-02 21:10:22 +08:00
parent 294498d8a7
commit 2d16502154
3 changed files with 189 additions and 12 deletions

View File

@@ -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<String> translate(String text, String to);
}
class GoogleTranslator implements Translator {
final Dio _dio = AppDio();
String get url => 'https://translate.google.com/translate_a/single';
Map<String, dynamic> 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<String> 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<String> translate(String text, String to) async {
final lines = text.split('\n');
var buffer = StringBuffer();
var result = '';
for(int i=0; i<lines.length; i++) {
final line = lines[i];
if (buffer.length + line.length > 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;
}
}

View File

@@ -646,7 +646,7 @@ class _NovelPageWithIdState extends LoadingState<NovelPageWithId, Novel> {
} }
class _RelatedNovelsPage extends StatefulWidget { class _RelatedNovelsPage extends StatefulWidget {
const _RelatedNovelsPage(this.id, {super.key}); const _RelatedNovelsPage(this.id);
final String id; final String id;

View File

@@ -7,7 +7,9 @@ import 'package:pixes/components/page_route.dart';
import 'package:pixes/components/title_bar.dart'; import 'package:pixes/components/title_bar.dart';
import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/app.dart';
import 'package:pixes/foundation/image_provider.dart'; import 'package:pixes/foundation/image_provider.dart';
import 'package:pixes/foundation/log.dart';
import 'package:pixes/network/network.dart'; import 'package:pixes/network/network.dart';
import 'package:pixes/network/translator.dart';
import 'package:pixes/pages/image_page.dart'; import 'package:pixes/pages/image_page.dart';
import 'package:pixes/pages/main_page.dart'; import 'package:pixes/pages/main_page.dart';
import 'package:pixes/utils/ext.dart'; import 'package:pixes/utils/ext.dart';
@@ -27,15 +29,36 @@ class _NovelReadingPageState extends LoadingState<NovelReadingPage, String> {
bool isShowingSettings = false; bool isShowingSettings = false;
String? translatedContent;
@override @override
void initState() { void initState() {
action = TitleBarAction(MdIcons.tune, "Settings".tl, () { action = TitleBarAction(MdIcons.tune, "Settings".tl, () {
if (!isShowingSettings) { if (!isShowingSettings) {
_NovelReadingSettings.show(context, () { _NovelReadingSettings.show(
context,
() {
setState(() {}); setState(() {});
}).then((value) { },
isShowingSettings = false; TranslationController(
content: data!,
isTranslated: translatedContent != null,
onTranslated: (s) {
setState(() {
translatedContent = s;
}); });
},
revert: () {
setState(() {
translatedContent = null;
});
},
),
).then(
(value) {
isShowingSettings = false;
},
);
isShowingSettings = true; isShowingSettings = true;
} else { } else {
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -92,7 +115,7 @@ class _NovelReadingPageState extends LoadingState<NovelReadingPage, String> {
); );
yield const SizedBox(height: 12.0); yield const SizedBox(height: 12.0);
var novelContent = data!.split('\n'); var novelContent = (translatedContent ?? data!).split('\n');
for (var content in novelContent) { for (var content in novelContent) {
if (content.isEmpty) continue; if (content.isEmpty) continue;
if (content.startsWith('[uploadedimage:')) { if (content.startsWith('[uploadedimage:')) {
@@ -132,14 +155,38 @@ class _NovelReadingPageState extends LoadingState<NovelReadingPage, String> {
} }
} }
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 { class _NovelReadingSettings extends StatefulWidget {
const _NovelReadingSettings(this.callback); const _NovelReadingSettings(this.callback, this.controller);
final void Function() callback; final void Function() callback;
static Future show(BuildContext context, void Function() callback) { final TranslationController controller;
return Navigator.of(context)
.push(SideBarRoute(_NovelReadingSettings(callback))); static Future show(
BuildContext context,
void Function() callback,
TranslationController controller,
) {
return Navigator.of(context).push(
SideBarRoute(_NovelReadingSettings(callback, controller)),
);
} }
@override @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());
}
}
} }