mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-05 01:27:49 +08:00
Add translation support to reply items (#1894)
* Add translation support to reply items - Add translateState and showTranslate fields to ReplyControl - Add translatedText field to ReplyInfo for translation results - Implement TranslateReplyReq message and translateReply API method - Add translation UI with loading state and result display in reply items - Show translation button when showTranslate is true and translateState is 2 * refa Signed-off-by: dom <githubaccount56556@proton.me> --------- Co-authored-by: dom <githubaccount56556@proton.me>
This commit is contained in:
@@ -13,6 +13,8 @@ import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart';
|
||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show ReplyInfo, ReplyControl, Content, Url;
|
||||
import 'package:PiliPlus/grpc/reply.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/reply.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
@@ -290,6 +292,7 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, ThemeData theme) {
|
||||
final replyControl = replyItem.replyControl;
|
||||
final padding = EdgeInsets.only(left: replyLevel == 0 ? 6 : 45, right: 6);
|
||||
return Column(
|
||||
mainAxisSize: .min,
|
||||
@@ -308,7 +311,7 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
maxLines: replyLevel == 1 ? replyLengthLimit : null,
|
||||
TextSpan(
|
||||
children: [
|
||||
if (replyItem.replyControl.isUpTop) ...[
|
||||
if (replyControl.isUpTop) ...[
|
||||
const WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: PBadge(
|
||||
@@ -322,7 +325,14 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
),
|
||||
const TextSpan(text: ' '),
|
||||
],
|
||||
_buildMessage(context, theme, replyItem),
|
||||
_buildMessage(
|
||||
context,
|
||||
theme,
|
||||
replyControl.showTranslation
|
||||
? replyItem.translatedContent
|
||||
: replyItem.content,
|
||||
replyControl,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -347,7 +357,7 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
],
|
||||
if (replyLevel != 0) ...[
|
||||
const SizedBox(height: 4),
|
||||
buttonAction(context, theme, replyItem.replyControl),
|
||||
buttonAction(context, theme, replyControl),
|
||||
],
|
||||
if (replyLevel == 1 && replyItem.count > Int64.ZERO) ...[
|
||||
Padding(
|
||||
@@ -359,20 +369,87 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTranslateBtn(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ReplyControl replyControl,
|
||||
TextStyle textStyle,
|
||||
ButtonStyle buttonStyle,
|
||||
) {
|
||||
late bool isProcessing = false;
|
||||
final color = replyControl.showTranslation
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outline.withValues(alpha: 0.8);
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: TextButton(
|
||||
style: buttonStyle,
|
||||
onPressed: () async {
|
||||
if (replyControl.showTranslation) {
|
||||
replyControl.showTranslation = false;
|
||||
(context as Element).markNeedsBuild();
|
||||
} else {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (replyItem.hasTranslatedContent()) {
|
||||
replyControl.showTranslation = true;
|
||||
(context as Element).markNeedsBuild();
|
||||
return;
|
||||
}
|
||||
isProcessing = true;
|
||||
final res = await ReplyGrpc.translateReply(
|
||||
type: replyItem.type,
|
||||
oid: replyItem.oid,
|
||||
rpid: replyItem.id,
|
||||
);
|
||||
if (res case Success(:final response)) {
|
||||
final item = response.translatedReply[replyItem.id];
|
||||
if (item != null && item.hasTranslatedContent()) {
|
||||
replyControl.showTranslation = true;
|
||||
replyItem.translatedContent = item.translatedContent;
|
||||
if (context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast('翻译结果为空');
|
||||
}
|
||||
} else if (res case Error(:final errMsg)) {
|
||||
SmartDialog.showToast('翻译失败: $errMsg');
|
||||
}
|
||||
isProcessing = false;
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
spacing: 3,
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
Icon(Icons.translate, size: 16, color: color),
|
||||
Text(
|
||||
replyControl.showTranslation ? '原文' : '翻译',
|
||||
style: textStyle.copyWith(color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buttonAction(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ReplyControl replyControl,
|
||||
) {
|
||||
final textStyle = TextStyle(
|
||||
fontSize: theme.textTheme.labelMedium!.fontSize,
|
||||
height: 1,
|
||||
fontWeight: .normal,
|
||||
color: theme.colorScheme.outline,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: theme.textTheme.labelMedium!.fontSize,
|
||||
);
|
||||
final buttonStyle = TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
const buttonStyle = ButtonStyle(
|
||||
visualDensity: .compact,
|
||||
tapTargetSize: .shrinkWrap,
|
||||
padding: WidgetStatePropertyAll(.zero),
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
@@ -386,20 +463,30 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
onReply?.call(replyItem);
|
||||
},
|
||||
child: Row(
|
||||
spacing: 3,
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.reply,
|
||||
size: 18,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.8),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Text('回复', style: textStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
if (replyItem.replyControl.cardLabels.isNotEmpty) ...[
|
||||
if (replyControl.translationSwitch == 2) ...[
|
||||
_buildTranslateBtn(
|
||||
context,
|
||||
theme,
|
||||
replyControl,
|
||||
textStyle,
|
||||
buttonStyle,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
] else if (replyItem.replyControl.cardLabels.isNotEmpty) ...[
|
||||
Text(
|
||||
replyItem.replyControl.cardLabels
|
||||
.map((e) => e.textContent)
|
||||
@@ -536,7 +623,12 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
? ''
|
||||
: ' ',
|
||||
),
|
||||
_buildMessage(context, theme, childReply),
|
||||
_buildMessage(
|
||||
context,
|
||||
theme,
|
||||
childReply.content,
|
||||
childReply.replyControl,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -585,9 +677,9 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
InlineSpan _buildMessage(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ReplyInfo replyItem,
|
||||
Content content,
|
||||
ReplyControl replyControl,
|
||||
) {
|
||||
final Content content = replyItem.content;
|
||||
final List<InlineSpan> spanChildren = <InlineSpan>[];
|
||||
bool hasNote = false;
|
||||
|
||||
@@ -826,9 +918,7 @@ class ReplyItemGrpc extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasNote &&
|
||||
replyItem.replyControl.isNote &&
|
||||
replyItem.replyControl.isNoteV2) {
|
||||
if (!hasNote && replyControl.isNote && replyControl.isNoteV2) {
|
||||
final Color color;
|
||||
NoDeadlineTapGestureRecognizer? recognizer;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user