You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1079 lines
39 KiB

import 'dart:async';
import 'dart:ui';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../controllers/translation_controller.dart';
import '../models/translation_models.dart';
import '../views/translation_history_view.dart';
import '../bindings/translation_history_binding.dart';
import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
import '../../../core/utils/background_session_notifier.dart';
import '../../common/widgets/language_selector_bar.dart';
// 导入拆分后的组件
import 'widgets/draggable_floating_button.dart'; // 可拖拽的悬浮按钮
import 'widgets/translation_item_widget.dart'; // 翻译项组件
import 'dialogs/language_selection_dialog.dart'; // 语言选择对话框
import 'dialogs/translation_mode_selector.dart'; // 翻译模式选择器
import 'dialogs/translation_long_press_menu.dart'; // 长按菜单
// import 'package:intl/intl.dart';
/// 翻译页退出操作(仅通话翻译且识别中才会弹出)
enum _TranslationExitAction { continueInBackground, end, cancel }
//主页面
class TranslationView extends GetView<TranslationController> {
/// 字幕小窗(PiP)的样式设置。
///
/// ⚠️ 这里只有两项,是因为 iOS 的 PiP 就只开放了这么点:
/// - **小窗实际大小由用户捏合手势决定**,App 无权直接设定。这里的「大小」
/// 改的是画布宽高比,系统会按新形状重新布局,观感上就是变宽/变窄。
/// - **透明度做不到透出下面的 App**。iOS 的 PiP 窗口本身不透明,系统没有
/// 开放任何接口。这个滑块只让卡片底色深浅不同。真要少挡内容就把它调小。
/// - 小窗上那两颗播放/暂停按钮的**尺寸也不可控**,是系统 chrome,
/// 它随小窗一起缩放——所以「按钮小一点」等价于「把小窗调小」。
void _showPipSettings(BuildContext context, bool isDarkMode) {
Get.bottomSheet(
Container(
padding: EdgeInsets.fromLTRB(20.w, 16.h, 20.w, 28.h),
decoration: BoxDecoration(
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('pipSettings'.tr,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87)),
SizedBox(height: 12.h),
_pipSlider(
label: 'pipSize'.tr,
isDarkMode: isDarkMode,
value: () => controller.pipWidthScale.value,
min: 0.5,
max: 2.0,
onChanged: controller.setPipWidthScale,
),
_pipSlider(
label: 'pipOpacity'.tr,
isDarkMode: isDarkMode,
value: () => controller.pipOpacity.value,
min: 0.2,
max: 1.0,
onChanged: controller.setPipOpacity,
),
],
),
),
isScrollControlled: true,
);
}
Widget _pipSlider({
required String label,
required bool isDarkMode,
required double Function() value,
required double min,
required double max,
required Future<void> Function(double) onChanged,
}) {
return Obx(() => Row(
children: [
SizedBox(
width: 72.w,
child: Text(label,
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.white70 : Colors.black54)),
),
Expanded(
child: Slider(
value: value().clamp(min, max),
min: min,
max: max,
onChanged: (v) => onChanged(v),
),
),
SizedBox(
width: 44.w,
child: Text('${(value() * 100).round()}%',
textAlign: TextAlign.end,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white54 : Colors.black45)),
),
],
));
}
const TranslationView({Key? key}) : super(key: key);
// 本地交互状态:当前显示操作按钮的消息时间戳(不改动控制器)
static final Rx<DateTime?> _activeActionTimestamp = Rx<DateTime?>(null);
// 切换当前操作按钮展示的项(再次点击同一项则隐藏)
void _toggleActiveActionItem(TranslationItem item) {
final DateTime ts = item.timestamp;
final DateTime? current = _activeActionTimestamp.value;
_activeActionTimestamp.value =
(current != null && current.isAtSameMomentAs(ts)) ? null : ts;
}
void _deleteControllerAfterPop() {
Future.delayed(const Duration(milliseconds: 400), () {
if (Get.isRegistered<TranslationController>()) {
Get.delete<TranslationController>(force: true);
}
});
}
/// 通话翻译退出确认弹窗:识别中点返回(而不是点结束)时弹,
/// 「后台继续」不停识别、也不删这个 permanent controller。
Future<_TranslationExitAction?> _showExitConfirmDialog(bool isDarkMode) {
return Get.dialog<_TranslationExitAction>(
AlertDialog(
title: Text('translationCallExitTitle'.tr),
content: Text('translationCallExitMessage'.tr),
actions: [
TextButton(
onPressed: () =>
Get.back(result: _TranslationExitAction.cancel),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () => Get.back(result: _TranslationExitAction.end),
child: Text('translationCallEnd'.tr),
),
TextButton(
onPressed: () => Get.back(
result: _TranslationExitAction.continueInBackground,
),
child: Text('translationContinueInBackground'.tr),
),
],
),
barrierDismissible: true,
);
}
Future<void> _handlePop(bool isDarkMode) async {
// 只有通话翻译在识别中才需要问;其它模式、或通话翻译还没开始识别,
// 跟以前一样直接停止退出。
final isActiveCall = controller.currentMode.value == 'call' &&
(controller.isRecognizing.value || controller.isPreparing.value);
if (!isActiveCall) {
await controller.stopAll();
Get.back();
_deleteControllerAfterPop();
return;
}
final action = await _showExitConfirmDialog(isDarkMode);
switch (action) {
case _TranslationExitAction.end:
await controller.stopAll();
Get.back();
_deleteControllerAfterPop();
return;
case _TranslationExitAction.continueInBackground:
final route = Get.currentRoute;
final arguments = Get.arguments as Map<String, dynamic>? ??
{'translationId': 'call'};
Get.back();
unawaited(BackgroundSessionNotifier.show(
id: kCallTranslationNotificationId,
title: 'callTranslation'.tr,
body: 'backgroundTranslationIndicatorBody'.tr,
route: route,
arguments: arguments,
));
return;
case _TranslationExitAction.cancel:
case null:
return;
}
}
@override
Widget build(BuildContext context) {
// 从后台通知/其它页面点回来了,之前挂着的"还在后台翻译"提示可以撤了。
if (controller.currentMode.value == 'call') {
unawaited(BackgroundSessionNotifier.cancel(kCallTranslationNotificationId));
}
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Obx(() {
// 面对面翻译原来是上下分屏 + 两个「按住说话」按钮,
// 现在与同声翻译统一:同一套 Scaffold、同一个开始/停止按钮,
// 左右语言胶囊分别代表左耳 / 右耳。
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
await _handlePop(isDarkMode);
},
child: Scaffold(
backgroundColor:
isDarkMode ? AppColors.darkBackground : Colors.grey[50],
appBar: _buildAppBar(context, isDarkMode),
body: SafeArea(
top: false,
child: Stack(
children: [
_buildBody(isDarkMode),
// 可拖拽的悬浮按钮
// DraggableFloatingButton(
// isDarkMode: isDarkMode,
// controller: controller,
// ),
],
),
),
),
);
});
}
PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
elevation: 1,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
// 走 maybePop 而不是直接 stopAll()+Get.back(),让上面的 PopScope
// 统一拦截判断:识别中需要弹「后台继续/结束」确认。
onPressed: () => Navigator.of(context).maybePop(),
),
title: Obx(() => GestureDetector(
onTap: () {
TranslationModeSelector.show(isDarkMode);
},
child: Text(
controller.currentModeTitle.value.tr,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
)),
centerTitle: true,
actions: [
Obx(() {
if (controller.isRecording.value) {
// 录音中:显示红色计时器样式,点击可停止录音
return Padding(
padding: EdgeInsets.only(right: 16.w),
child: GestureDetector(
onTap: controller.toggleisRecord, // 点击结束录音
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(20.r),
),
child: Row(
children: [
Icon(
Icons.fiber_manual_record,
color: Colors.white,
size: 18.sp,
),
SizedBox(width: 4.w),
Text(
controller.recordDurationText.value,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
} else {
// 未录音:显示红色录音按钮 + 文件夹按钮 + 悬浮窗按钮
return Row(
children: [
// 悬浮窗切换按钮(仅在支持的模式下显示)
Obx(() {
if (controller.currentMode.value == 'call' ||
controller.currentMode.value == 'audioVideo') {
return IconButton(
icon: Icon(
controller.isFloatingWindowEnabled.value
? Icons.picture_in_picture
: Icons.picture_in_picture_alt_outlined,
color: controller.isFloatingWindowEnabled.value
? Colors.blue
: (isDarkMode ? Colors.white : Colors.black),
size: 24.sp,
),
onPressed: controller.toggleFloatingWindow,
tooltip: controller.isFloatingWindowEnabled.value
? 'disableFloatingWindow'.tr
: 'enableFloatingWindow'.tr,
);
}
return const SizedBox.shrink();
}),
// 小窗样式设置。单独一颗按钮而不是长按同一颗——长按是隐藏
// 交互,用户发现不了,而这两项恰恰是需要一边看效果一边调的。
Obx(() {
// ⚠️ 可观测变量必须在平台判断**之前**读。
// 写成 `Platform.isIOS && controller.currentMode.value == ...`
// 时,Dart 的 && 在安卓上短路,右半边压根不执行,这个 Obx 就
// 一个可观测变量都没订阅到——GetX 判定「用法不当」直接抛错,
// Flutter 用错误控件顶替整条 AppBar,表现为标题和返回键被一块
// 灰/红色的框盖住(release 是灰的,看着像一层阴影)。
// 只在安卓复现:iOS 上 Platform.isIOS 为真,右半边照常执行。
final mode = controller.currentMode.value;
if (Platform.isIOS &&
(mode == 'call' || mode == 'audioVideo')) {
return IconButton(
icon: Icon(
Icons.tune,
color: isDarkMode ? Colors.white70 : Colors.black54,
size: 20.sp,
),
onPressed: () => _showPipSettings(context, isDarkMode),
tooltip: 'pipSettings'.tr,
);
}
return const SizedBox.shrink();
}),
IconButton(
icon: Icon(
Icons.radio_button_checked,
color: Colors.red,
size: 24.sp,
),
onPressed: controller.toggleisRecord,
tooltip: 'enableRecording'.tr, // 是否开启录音
),
IconButton(
icon: Icon(
Icons.folder,
color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp,
),
onPressed: () async {
await controller.stopAll();
Get.toNamed(Routes.translationList);
controller.setupScrollListener();
},
tooltip: 'viewAudioFiles'.tr, // 查看录音文件
),
// 历史记录按钮
// IconButton(
// icon: Icon(
// Icons.history,
// color: isDarkMode ? Colors.white : Colors.black,
// size: 24.sp,
// ),
// onPressed: () async {
// await controller.stopAll();
// Get.toNamed(Routes.translationHistory);
// },
// tooltip: 'translationHistory'.tr, // 翻译历史
// ),
],
);
}
}),
],
bottom: PreferredSize(
preferredSize: Size.fromHeight(
controller.currentMode.value == 'call' ? 70.h : 50.h,
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 统一通用语言选择栏组件
LanguageSelectorBar(
sourceText: () => controller.sourceLanguage.value.split(' ')[0],
targetText: () => controller.targetLanguage.value.split(' ')[0],
onTapSource: () => LanguageSelectionDialog.show(
true, isDarkMode,
excludeLanguage: controller.targetLanguage.value,
translationController: controller),
onTapTarget: () => LanguageSelectionDialog.show(
false, isDarkMode,
excludeLanguage: controller.sourceLanguage.value,
translationController: controller),
onSwap: controller.swapLanguages,
translateLanguageNames: true, // 是否翻译语言名称
fontSizeSp: 15,
// 面对面翻译:左边胶囊 = 左耳,右边胶囊 = 右耳。
// 两只耳朵各自朝向一位说话人,所以要在语言旁边标清楚是哪一只。
sourceSuffix: controller.currentMode.value == 'faceToFace'
? _earTag('faceToFaceLeftEar'.tr, isDarkMode)
: null,
targetSuffix: controller.currentMode.value == 'call'
? Icon(
Icons.phone_in_talk,
size: 20.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600],
)
: controller.currentMode.value == 'faceToFace'
? _earTag('faceToFaceRightEar'.tr, isDarkMode)
: null,
),
// 通话模式下的“对方 / 我”标签行
if (controller.currentMode.value == 'call') ...[
SizedBox(height: 6.h),
Row(
children: [
Expanded(
child: Text(
'me'.tr, //我
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
// 预留与上方互换按钮大致等宽的间距,保证对齐
SizedBox(width: 44.w),
Expanded(
child: Text(
'counterPart'.tr, //对方
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
),
],
],
),
),
),
);
}
/// 面对面翻译里标注这一侧对应哪只耳机
Widget _earTag(String text, bool isDarkMode) {
return Container(
margin: EdgeInsets.only(left: 4.w),
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.h),
decoration: BoxDecoration(
color: (isDarkMode ? Colors.white : Colors.black).withOpacity(0.08),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
text,
style: TextStyle(
fontSize: 10.sp,
height: 1.2,
color: isDarkMode ? Colors.white70 : Colors.grey[700],
),
),
);
}
Widget _buildBody(bool isDarkMode) {
return
// GestureDetector(
// onPanUpdate: (details) {
// // 检测左滑手势
// if (details.delta.dx < -10) {
// // 左滑距离超过10像素时触发
// _handleSwipeLeft();
// }
// },
// child:
Column(
children: [
Expanded(
child: _buildTranslationContent(isDarkMode),
),
_buildControlBar(isDarkMode),
],
);
// );
}
// 处理左滑手势
// void _handleSwipeLeft() async {
// await controller.stopAll();
// Get.toNamed(Routes.translationHistory);
// }
Widget _buildTranslationContent(bool isDarkMode) {
return Obx(() {
// 其他模式使用原有的列表布局
final itemCount = controller.translationHistory.length +
(controller.hasMoreHistory.value ? 1 : 0);
return ListView.builder(
controller: controller.scrollController,
reverse: true, // 🔑 关键优化:采用reverse模式,参考Agent模块
padding: EdgeInsets.only(
left: 16.w,
right: 16.w,
top: 16.h,
bottom: 16.h,
),
itemCount: itemCount,
itemBuilder: (context, index) {
// 如果是最后一项且有更多历史记录,显示加载指示器(reverse模式下)
if (index == controller.translationHistory.length &&
controller.hasMoreHistory.value) {
return _buildLoadMoreIndicator();
}
// reverse模式下需要反转索引来获取正确的历史记录
final historyIndex = controller.translationHistory.length - 1 - index;
// 检查索引是否有效(reverse模式下的索引验证)
if (historyIndex < 0 ||
historyIndex >= controller.translationHistory.length) {
return const SizedBox.shrink();
}
// 获取当前项
final historyItem = controller.translationHistory[historyIndex];
// 构建项目widget
return TranslationItemWidget(
item: historyItem,
activeActionTimestamp: _activeActionTimestamp,
onToggleActionItem: _toggleActiveActionItem,
onLongPressStart: _onLongPressTranslationItem,
onDeleteItemConfirm: _showDeleteItemConfirmDialog,
);
},
);
});
}
Widget _buildControlBar(bool isDarkMode) {
// 使用GlobalKey来获取控制栏的高度
final controlBarKey = GlobalKey();
// 在构建完成后获取控制栏的高度并设置到控制器中
WidgetsBinding.instance.addPostFrameCallback((_) {
if (controlBarKey.currentContext != null) {
final RenderBox renderBox =
controlBarKey.currentContext!.findRenderObject() as RenderBox;
final height = renderBox.size.height;
controller.setBottomBarHeight(height);
}
});
return Container(
key: controlBarKey,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(isDarkMode ? 30 : 13),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 原有的按钮区域
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 主要控制按钮 - 开始/暂停语音识别
Obx(() {
final isRecognizing = controller.isRecognizing.value;
final isPreparing = controller.isPreparing.value;
return GestureDetector(
onTap: () {
if (isRecognizing) {
controller.stopAll();
} else {
controller.startRecognition();
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 60.w,
height: 60.w,
transform: isPreparing
? (Matrix4.identity()..scale(1.08))
: Matrix4.identity(),
decoration: BoxDecoration(
color: isRecognizing
? (isDarkMode ? Colors.red[700] : Colors.red)
: (isDarkMode ? AppColors.primary : Colors.blue),
shape: BoxShape.circle,
boxShadow: isPreparing
? [
BoxShadow(
color: (isDarkMode
? AppColors.primary
: Colors.blue)
.withOpacity(0.5),
blurRadius: 12,
spreadRadius: 2,
)
]
: [],
),
child: Icon(
isRecognizing ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 30.sp,
),
),
);
}),
],
),
// 添加间距
//SizedBox(height: 16.h),
// 语音识别结果显示框
// Container(
// width: double.infinity,
// padding: EdgeInsets.all(12.w),
// decoration: BoxDecoration(
// color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
// borderRadius: BorderRadius.circular(8.r),
// border: Border.all(
// color: isDarkMode ? Colors.grey[600]! : Colors.grey[300]!,
// width: 1,
// ),
// ),
// child: Obx(() {
// // 显示最新的语音识别结果
// final recognitionText = controller.latestRecognitionText.value;
// final detectedLanguage = controller.detectedLanguageCode1.value;
// return Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // 标题
// Text(
// '麦头语音识别结果',
// style: TextStyle(
// fontSize: 12.sp,
// color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
// fontWeight: FontWeight.w500,
// ),
// ),
// SizedBox(height: 4.h),
// // 识别文本 - 这里显示 finalResult1 的内容
// Text(
// recognitionText.isEmpty ? '等待语音输入...' : recognitionText,
// style: TextStyle(
// fontSize: 14.sp,
// color: isDarkMode ? Colors.white : Colors.black87,
// height: 1.3,
// ),
// ),
// // 检测到的语言(如果有)
// if (detectedLanguage.isNotEmpty) ...[
// SizedBox(height: 4.h),
// Text(
// '检测语言: $detectedLanguage',
// style: TextStyle(
// fontSize: 10.sp,
// color:
// isDarkMode ? Colors.grey[500] : Colors.grey[500],
// ),
// ),
// ],
// ],
// );
// }),
// ),
],
),
);
}
// 构建加载更多指示器
Widget _buildLoadMoreIndicator() {
return Obx(() {
if (controller.isLoadingMore.value) {
// 正在加载状态 - 增强视觉效果
return Container(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 24.w,
height: 24.h,
child: CircularProgressIndicator(
strokeWidth: 2.5.w,
valueColor: const AlwaysStoppedAnimation<Color>(
AppColors.primary,
),
),
),
const SizedBox(height: 12),
Text(
'loading'.tr, //加载中
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
],
),
),
);
} else if (controller.isLoadingMoreError.value) {
// 加载失败状态 - 增强视觉效果
return Container(
padding: const EdgeInsets.symmetric(vertical: 18),
child: Center(
child: GestureDetector(
onTap: () => controller.loadMoreHistory(),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.red[200]!, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.refresh,
size: 18.sp,
color: Colors.red[600],
),
SizedBox(width: 8.w),
Text(
'loadFailedClickRetry'.tr,
style: TextStyle(
fontSize: 14.sp,
color: Colors.red[600],
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
);
} else {
// 默认状态(有更多记录但未加载)- 增强视觉效果
return Container(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.keyboard_arrow_up,
size: 16.sp,
color: Colors.grey[500],
),
SizedBox(width: 4.w),
Text(
'swipeUpForMore'.tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
),
],
),
),
),
);
}
});
}
/// 处理翻译条目长按事件
void _onLongPressTranslationItem(
BuildContext context,
bool isDarkMode,
LongPressStartDetails details,
TranslationItem item,
) {
TranslationLongPressMenu.show(
context: context,
isDarkMode: isDarkMode,
details: details,
item: item,
onCopy: (item) => TranslationLongPressMenu.copyTranslationItem(item),
onDeleteItem: (item) => _deleteTranslationItem(item),
onClearHistory: _showClearHistoryConfirmDialog,
);
}
// 显示删除单条记录确认对话框
void _showDeleteItemConfirmDialog(TranslationItem item) {
final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'deleteTranslationRecord'.tr, // 删除翻译记录
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'deleteTranslationConfirm'.tr, // 确定要删除这条翻译记录吗?此操作无法撤销。
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
SizedBox(height: 12.h),
// 显示要删除的记录预览
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]?.withOpacity(0.5)
: Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.sourceText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (item.translatedText.isNotEmpty) ...[
SizedBox(height: 4.h),
Text(
item.translatedText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
// 删除指定的翻译记录
_deleteTranslationItem(item);
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
// 删除翻译记录的具体实现
void _deleteTranslationItem(TranslationItem item) {
try {
// 调用控制器的删除方法
controller.deleteTranslationItem(item.sessionId, item.timestamp);
// 显示删除成功提示
// Get.snackbar(
// 'success'.tr, // deleteSuccess
// '已删除该翻译记录', // deleteTranslationSuccess
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
// colorText: Get.isDarkMode ? Colors.white : Colors.black,
// duration: const Duration(milliseconds: 800),
// );
} catch (e) {
// 显示删除失败提示
Get.snackbar(
'error'.tr,
'deleteTranslationRecordError'.trParams({'error': e.toString()}),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(milliseconds: 1500),
);
}
}
// 显示删除所有记录确认对话框
void _showClearHistoryConfirmDialog() {
final isDarkMode = Get.isDarkMode;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'clearHistory'.tr,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Text(
'clearHistoryConfirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
// 清除历史记录
controller.clearTranslationHistory();
Get.back();
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
// 构建语言选择栏
Widget _buildLanguageSelectionBar(bool isDarkMode) {
return Container(
height: 44.h,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 6.h),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: LanguageSelectorBar(
sourceText: () => controller.sourceLanguage.value.split(' ')[0],
targetText: () => controller.targetLanguage.value.split(' ')[0],
onTapSource: () => LanguageSelectionDialog.show(true, isDarkMode,
excludeLanguage: controller.targetLanguage.value,
translationController: controller),
onTapTarget: () => LanguageSelectionDialog.show(false, isDarkMode,
excludeLanguage: controller.sourceLanguage.value,
translationController: controller),
onSwap: controller.swapLanguages,
translateLanguageNames: true, // 是否翻译语言名称
fontSizeSp: 15,
),
);
}
}