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 '../models/translation_direction.dart'; import '../views/translation_history_view.dart'; import '../bindings/translation_history_binding.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/logger.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 { /// 字幕小窗(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 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 _activeActionTimestamp = Rx(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()) { Get.delete(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, ); } /// 停止翻译,但**绝不让返回键卡死**。 /// /// ⚠️ 这个页面是 `PopScope(canPop: false)`,返回只能由这里的 `Get.back()` 放行。 /// 原来直接 `await controller.stopAll()`,而这条链末端是一串原生往返 /// (关 ASR 连接、收音频引擎、停录屏接收、归档),任何一处不返回, /// 返回键就变成永久空操作 —— 用户的说法是「退不出去」。 /// 现在给它一个上限:超时就先退出去,收尾让它在后台继续跑完。 static const Duration _stopAllMaxWait = Duration(seconds: 3); Future _stopAllSafely() async { try { await controller.stopAll().timeout(_stopAllMaxWait); } on TimeoutException { Logger.w('Translation', '停止翻译超过 ${_stopAllMaxWait.inSeconds}s 仍未返回,先退出页面,收尾继续在后台进行'); } catch (e) { Logger.e('Translation', '停止翻译失败(忽略,继续退出): $e'); } } Future _handlePop(bool isDarkMode) async { // 只有通话翻译在识别中才需要问;其它模式、或通话翻译还没开始识别, // 跟以前一样直接停止退出。 final isActiveCall = controller.currentMode.value == 'call' && (controller.isRecognizing.value || controller.isPreparing.value); if (!isActiveCall) { await _stopAllSafely(); Get.back(); _deleteControllerAfterPop(); return; } final action = await _showExitConfirmDialog(isDarkMode); switch (action) { case _TranslationExitAction.end: await _stopAllSafely(); Get.back(); _deleteControllerAfterPop(); return; case _TranslationExitAction.continueInBackground: final route = Get.currentRoute; final arguments = Get.arguments as Map? ?? {'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], // 翻译进行中不开弹窗(见 controller.guardLanguageChange) onTapSource: () { if (!controller.guardLanguageChange()) return; LanguageSelectionDialog.show(true, isDarkMode, excludeLanguage: controller.targetLanguage.value, translationController: controller); }, onTapTarget: () { if (!controller.guardLanguageChange()) return; LanguageSelectionDialog.show(false, isDarkMode, excludeLanguage: controller.sourceLanguage.value, translationController: controller); }, onSwap: controller.swapLanguages, // 同声翻译 / 多媒体翻译是单向的,图标跟着变成单向箭头, // 免得双向图标让人以为反着说也能翻(见 isOneWayTranslationMode) swapIcon: () => isOneWayTranslationMode(controller.currentMode.value) ? Icons.arrow_forward : Icons.swap_horiz, 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( 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, ), ); } }