Message translate

This commit is contained in:
2025-07-31 21:16:38 +08:00
parent 047c8d93aa
commit 4597373ac9

View File

@@ -5,16 +5,19 @@ import 'package:easy_localization/easy_localization.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_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/message.dart'; import 'package:island/database/message.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/models/embed.dart'; import 'package:island/models/embed.dart';
import 'package:island/pods/call.dart'; import 'package:island/pods/call.dart';
import 'package:island/pods/translate.dart';
import 'package:island/screens/chat/room.dart'; import 'package:island/screens/chat/room.dart';
import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/account_pfc.dart'; import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/alert.native.dart';
import 'package:island/widgets/content/cloud_file_collection.dart'; import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/embed/link.dart'; import 'package:island/widgets/content/embed/link.dart';
@@ -67,6 +70,46 @@ class MessageItem extends HookConsumerWidget {
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS); final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
final messageLanguage =
remoteMessage.content != null
? ref.watch(detectStringLanguageProvider(remoteMessage.content!))
: null;
final currentLanguage = context.locale.toString();
final translatableLanguage =
messageLanguage != null
? messageLanguage.substring(0, 2) != currentLanguage.substring(0, 2)
: false;
final translating = useState(false);
final translatedText = useState<String?>(null);
Future<void> translate() async {
if (translatedText.value != null) {
translatedText.value = null;
return;
}
if (translating.value) return;
if (remoteMessage.content == null) return;
translating.value = true;
try {
final text = await ref.watch(
translateStringProvider(
TranslateQuery(
text: remoteMessage.content!,
lang: currentLanguage.substring(0, 2),
),
).future,
);
translatedText.value = text;
} catch (err) {
showErrorAlert(err);
} finally {
translating.value = false;
}
}
return ContextMenuWidget( return ContextMenuWidget(
menuProvider: (_) { menuProvider: (_) {
if (onAction == null) return Menu(children: []); if (onAction == null) return Menu(children: []);
@@ -103,6 +146,18 @@ class MessageItem extends HookConsumerWidget {
onAction!.call(MessageItemAction.forward); onAction!.call(MessageItemAction.forward);
}, },
), ),
if (translatableLanguage) MenuSeparator(),
if (translatableLanguage)
MenuAction(
title:
translatedText.value == null
? 'translate'.tr()
: translating.value
? 'translating'.tr()
: 'translated'.tr(),
image: MenuImage.icon(Symbols.translate),
callback: translate,
),
if (isMobile) MenuSeparator(), if (isMobile) MenuSeparator(),
if (isMobile) if (isMobile)
MenuAction( MenuAction(
@@ -221,7 +276,10 @@ class MessageItem extends HookConsumerWidget {
isReply: false, isReply: false,
).padding(vertical: 4), ).padding(vertical: 4),
if (_MessageItemContent.hasContent(remoteMessage)) if (_MessageItemContent.hasContent(remoteMessage))
_MessageItemContent(item: remoteMessage), _MessageItemContent(
item: remoteMessage,
translatedText: translatedText.value,
),
if (remoteMessage.attachments.isNotEmpty) if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@@ -482,7 +540,8 @@ class MessageQuoteWidget extends HookConsumerWidget {
class _MessageItemContent extends StatelessWidget { class _MessageItemContent extends StatelessWidget {
final SnChatMessage item; final SnChatMessage item;
const _MessageItemContent({required this.item}); final String? translatedText;
const _MessageItemContent({required this.item, this.translatedText});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -495,10 +554,40 @@ class _MessageItemContent extends StatelessWidget {
); );
case 'text': case 'text':
default: default:
return MarkdownTextContent( return Column(
content: item.content!, mainAxisSize: MainAxisSize.min,
isSelectable: true, crossAxisAlignment: CrossAxisAlignment.start,
linesMargin: EdgeInsets.zero, children: [
MarkdownTextContent(
content: item.content!,
isSelectable: true,
linesMargin: EdgeInsets.zero,
),
if (translatedText?.isNotEmpty ?? false)
...([
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: math.min(
280,
MediaQuery.of(context).size.width * 0.4,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('translated').tr().fontSize(11).opacity(0.75),
const Gap(8),
Flexible(child: Divider()),
],
).padding(vertical: 4),
),
MarkdownTextContent(
content: translatedText!,
isSelectable: true,
linesMargin: EdgeInsets.zero,
),
]),
],
); );
} }
} }