✨ Complete translation
This commit is contained in:
parent
d2f4e7a969
commit
c9f69fed2c
@ -843,5 +843,8 @@
|
|||||||
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.",
|
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.",
|
||||||
"postCommentAdd": "Write a comment",
|
"postCommentAdd": "Write a comment",
|
||||||
"translate": "Translate",
|
"translate": "Translate",
|
||||||
"translated": "Translated"
|
"translating": "Translating…",
|
||||||
|
"translated": "Translated",
|
||||||
|
"settingsAutoTranslate": "Auto Translate",
|
||||||
|
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages."
|
||||||
}
|
}
|
||||||
|
@ -841,5 +841,8 @@
|
|||||||
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。",
|
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。",
|
||||||
"postCommentAdd": "撰写一条评论",
|
"postCommentAdd": "撰写一条评论",
|
||||||
"translate": "翻译",
|
"translate": "翻译",
|
||||||
"translated": "已翻译"
|
"translating": "正在翻译……",
|
||||||
|
"translated": "已翻译",
|
||||||
|
"settingsAutoTranslate": "自动翻译",
|
||||||
|
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。"
|
||||||
}
|
}
|
||||||
|
@ -841,5 +841,8 @@
|
|||||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||||
"postCommentAdd": "撰寫一條評論",
|
"postCommentAdd": "撰寫一條評論",
|
||||||
"translate": "翻譯",
|
"translate": "翻譯",
|
||||||
"translated": "已翻譯"
|
"translating": "正在翻譯……",
|
||||||
|
"translated": "已翻譯",
|
||||||
|
"settingsAutoTranslate": "自動翻譯",
|
||||||
|
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
|
||||||
}
|
}
|
||||||
|
@ -841,5 +841,8 @@
|
|||||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||||
"postCommentAdd": "撰寫一條評論",
|
"postCommentAdd": "撰寫一條評論",
|
||||||
"translate": "翻譯",
|
"translate": "翻譯",
|
||||||
"translated": "已翻譯"
|
"translating": "正在翻譯……",
|
||||||
|
"translated": "已翻譯",
|
||||||
|
"settingsAutoTranslate": "自動翻譯",
|
||||||
|
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ const kAppExpandChatLink = 'app_expand_chat_link';
|
|||||||
const kAppRealmCompactView = 'app_realm_compact_view';
|
const kAppRealmCompactView = 'app_realm_compact_view';
|
||||||
const kAppCustomFonts = 'app_custom_fonts';
|
const kAppCustomFonts = 'app_custom_fonts';
|
||||||
const kAppMixedFeed = 'app_mixed_feed';
|
const kAppMixedFeed = 'app_mixed_feed';
|
||||||
|
const kAppAutoTranslate = 'app_auto_translate';
|
||||||
|
|
||||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||||
'settingsImageQualityLowest': FilterQuality.none,
|
'settingsImageQualityLowest': FilterQuality.none,
|
||||||
@ -86,6 +87,15 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
return prefs.getBool(kAppMixedFeed) ?? true;
|
return prefs.getBool(kAppMixedFeed) ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get autoTranslate {
|
||||||
|
return prefs.getBool(kAppAutoTranslate) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set autoTranslate(bool value) {
|
||||||
|
prefs.setBool(kAppAutoTranslate, value);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
set mixedFeed(bool value) {
|
set mixedFeed(bool value) {
|
||||||
prefs.setBool(kAppMixedFeed, value);
|
prefs.setBool(kAppMixedFeed, value);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -387,6 +387,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
.fontSize(17)
|
.fontSize(17)
|
||||||
.tr()
|
.tr()
|
||||||
.padding(horizontal: 20, bottom: 4),
|
.padding(horizontal: 20, bottom: 4),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.translate),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
title: Text('settingsAutoTranslate').tr(),
|
||||||
|
subtitle: Text('settingsAutoTranslateDescription').tr(),
|
||||||
|
value: _prefs.getBool(kAppAutoTranslate) ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_prefs.setBool(kAppAutoTranslate, value ?? false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
secondary: const Icon(Symbols.vibration),
|
secondary: const Icon(Symbols.vibration),
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
@ -246,9 +246,14 @@ class _ChatMessageText extends StatefulWidget {
|
|||||||
class _ChatMessageTextState extends State<_ChatMessageText> {
|
class _ChatMessageTextState extends State<_ChatMessageText> {
|
||||||
late String _displayText = widget.data.body['text'] ?? '';
|
late String _displayText = widget.data.body['text'] ?? '';
|
||||||
bool _isTranslated = false;
|
bool _isTranslated = false;
|
||||||
|
bool _isTranslating = false;
|
||||||
|
|
||||||
Future<void> _translateText() async {
|
Future<void> _translateText() async {
|
||||||
|
if (widget.data.body['text'] == null || widget.data.body['text']!.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final ta = context.read<SnTranslator>();
|
final ta = context.read<SnTranslator>();
|
||||||
|
setState(() => _isTranslating = true);
|
||||||
try {
|
try {
|
||||||
final to = EasyLocalization.of(context)!.locale.languageCode;
|
final to = EasyLocalization.of(context)!.locale.languageCode;
|
||||||
_displayText = await ta.translate(
|
_displayText = await ta.translate(
|
||||||
@ -259,6 +264,19 @@ class _ChatMessageTextState extends State<_ChatMessageText> {
|
|||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (mounted) context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isTranslating = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
if (cfg.autoTranslate) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
_translateText();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,6 +363,16 @@ class _ChatMessageTextState extends State<_ChatMessageText> {
|
|||||||
),
|
),
|
||||||
if (widget.data.updatedAt != widget.data.createdAt)
|
if (widget.data.updatedAt != widget.data.createdAt)
|
||||||
Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75),
|
Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75),
|
||||||
|
if (_isTranslating)
|
||||||
|
AnimateWidgetExtensions(Text('translating').tr())
|
||||||
|
.animate(onPlay: (e) => e.repeat())
|
||||||
|
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
|
||||||
|
.then()
|
||||||
|
.fadeOut(
|
||||||
|
duration: 500.ms,
|
||||||
|
delay: 1000.ms,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
),
|
||||||
if (_isTranslated)
|
if (_isTranslated)
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Text('translated').tr().opacity(0.75),
|
child: Text('translated').tr().opacity(0.75),
|
||||||
|
@ -8,6 +8,7 @@ import 'package:file_saver/file_saver.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@ -152,6 +153,49 @@ class _PostItemState extends State<PostItem> {
|
|||||||
late String _displayTitle = widget.data.body['title'] ?? '';
|
late String _displayTitle = widget.data.body['title'] ?? '';
|
||||||
late String _displayDescription = widget.data.body['description'] ?? '';
|
late String _displayDescription = widget.data.body['description'] ?? '';
|
||||||
bool _isTranslated = false;
|
bool _isTranslated = false;
|
||||||
|
bool _isTranslating = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
if (cfg.autoTranslate) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
_translateText();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _translateText() async {
|
||||||
|
final ta = context.read<SnTranslator>();
|
||||||
|
setState(() => _isTranslating = true);
|
||||||
|
try {
|
||||||
|
final to = EasyLocalization.of(context)!.locale.languageCode;
|
||||||
|
final futures = List<Future<void>>.empty(growable: true);
|
||||||
|
if (_displayTitle.isNotEmpty) {
|
||||||
|
futures.add(ta.translate(_displayTitle, to: to).then((value) {
|
||||||
|
_displayTitle = value;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (_displayDescription.isNotEmpty) {
|
||||||
|
futures.add(ta.translate(_displayDescription, to: to).then((value) {
|
||||||
|
_displayDescription = value;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (_displayText.isNotEmpty) {
|
||||||
|
futures.add(ta.translate(_displayText, to: to).then((value) {
|
||||||
|
_displayText = value;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
await Future.wait(futures);
|
||||||
|
_isTranslated = true;
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isTranslating = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onChanged(SnPost data) {
|
void _onChanged(SnPost data) {
|
||||||
if (widget.onChanged != null) widget.onChanged!(data);
|
if (widget.onChanged != null) widget.onChanged!(data);
|
||||||
@ -280,14 +324,8 @@ class _PostItemState extends State<PostItem> {
|
|||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
widget.onDeleted?.call();
|
widget.onDeleted?.call();
|
||||||
},
|
},
|
||||||
onTranslate: (text) {
|
onTranslate: () {
|
||||||
setState(() {
|
_translateText();
|
||||||
_displayText = text['content']?.trim() ?? '';
|
|
||||||
_displayTitle = text['title']?.trim() ?? '';
|
|
||||||
_displayDescription =
|
|
||||||
text['description']?.trim() ?? '';
|
|
||||||
_isTranslated = true;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -384,6 +422,23 @@ class _PostItemState extends State<PostItem> {
|
|||||||
.plural(widget.data.totalViews),
|
.plural(widget.data.totalViews),
|
||||||
],
|
],
|
||||||
).opacity(0.75),
|
).opacity(0.75),
|
||||||
|
if (_isTranslating)
|
||||||
|
AnimateWidgetExtensions(Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.translate, size: 20),
|
||||||
|
const Gap(4),
|
||||||
|
Text('translating').tr(),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.animate(onPlay: (e) => e.repeat())
|
||||||
|
.fadeIn(
|
||||||
|
duration: 500.ms, curve: Curves.easeOut)
|
||||||
|
.then()
|
||||||
|
.fadeOut(
|
||||||
|
duration: 500.ms,
|
||||||
|
delay: 1000.ms,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
),
|
||||||
if (_isTranslated)
|
if (_isTranslated)
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -407,7 +462,11 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(
|
).padding(
|
||||||
bottom: widget.showViews || _isTranslated ? 8 : 0,
|
bottom: widget.showViews ||
|
||||||
|
_isTranslated ||
|
||||||
|
_isTranslating
|
||||||
|
? 8
|
||||||
|
: 0,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -989,7 +1048,7 @@ class _PostActionPopup extends StatelessWidget {
|
|||||||
final Function onDeleted;
|
final Function onDeleted;
|
||||||
final Function() onShare, onShareImage;
|
final Function() onShare, onShareImage;
|
||||||
final Function()? onSelectAnswer;
|
final Function()? onSelectAnswer;
|
||||||
final Function(Map<String, dynamic>)? onTranslate;
|
final Function()? onTranslate;
|
||||||
const _PostActionPopup({
|
const _PostActionPopup({
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.isAuthor,
|
required this.isAuthor,
|
||||||
@ -1041,28 +1100,6 @@ class _PostActionPopup extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _translatePost(BuildContext context) async {
|
|
||||||
final ta = context.read<SnTranslator>();
|
|
||||||
try {
|
|
||||||
final to = EasyLocalization.of(context)!.locale.languageCode;
|
|
||||||
final body = {
|
|
||||||
'title': (data.body['title']?.isNotEmpty ?? false)
|
|
||||||
? await ta.translate(data.body['title'], to: to)
|
|
||||||
: null,
|
|
||||||
'description': (data.body['description']?.isNotEmpty ?? false)
|
|
||||||
? await ta.translate(data.body['description'], to: to)
|
|
||||||
: null,
|
|
||||||
'content': (data.body['content']?.isNotEmpty ?? false)
|
|
||||||
? await ta.translate(data.body['content'], to: to)
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
onTranslate?.call(body);
|
|
||||||
} catch (err) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
@ -1084,7 +1121,7 @@ class _PostActionPopup extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_translatePost(context);
|
onTranslate?.call();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (onTranslate != null) PopupMenuDivider(),
|
if (onTranslate != null) PopupMenuDivider(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user