Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

weicu
tanlongsheng 1 year ago
parent
commit
71dee5e4c8
  1. 3
      lib/core/bindings/initial_binding.dart
  2. 95
      lib/core/themes/language/lang/en_us.dart
  3. 93
      lib/core/themes/language/lang/zh_cn.dart
  4. 18
      lib/core/themes/language/translation_service.dart
  5. 78
      lib/core/translations/language/zh_cn.dart
  6. 38
      lib/data/services/ble_manager.dart
  7. 4
      lib/data/services/music_manager.dart
  8. 318
      lib/modules/agent/controllers/agent_controller.dart
  9. 83
      lib/modules/agent/views/agent_view.dart
  10. 2
      lib/modules/agent/views/music_card.dart
  11. 4
      lib/modules/agent/widgets/sound_bar_widget.dart
  12. 15
      lib/modules/music/views/music_playlist_view.dart
  13. 851
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  14. 46
      lib/modules/pairing/views/pairing_view.dart
  15. 14
      lib/modules/pairing/views/pairing_view_new.dart
  16. 2
      lib/modules/settings/views/settings_view.dart
  17. 223
      lib/modules/translation/controllers/translation_controller.dart
  18. 64
      lib/modules/translation/controllers/translation_detail_controller.dart
  19. 50
      lib/modules/translation/controllers/translation_history_controller.dart
  20. 313
      lib/modules/translation/views/translation_detail_view.dart
  21. 134
      lib/modules/translation/views/translation_history_view.dart
  22. 28
      lib/modules/translation/views/translation_list_view.dart
  23. 21
      lib/modules/translation/views/translation_view.dart
  24. 20
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  25. 18
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  26. 46
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  27. 11
      local_plugins/music_service/lib/music_service.dart
  28. 8
      local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt

3
lib/core/bindings/initial_binding.dart

@ -4,6 +4,7 @@ import 'package:deep_voice/data/services/navigation_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../data/services/speech_factory.dart'; import '../../data/services/speech_factory.dart';
import '../../data/services/volcano_translation_service.dart'; import '../../data/services/volcano_translation_service.dart';
import '../../data/services/volcano_ai_service.dart';
import '../../data/services/language_manager.dart'; import '../../data/services/language_manager.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import '../../data/services/bluetooth_manager.dart'; import '../../data/services/bluetooth_manager.dart';
@ -40,7 +41,7 @@ class InitialBinding extends Bindings {
Get.lazyPut<NavigationManager>(() => NavigationManager(), fenix: true); Get.lazyPut<NavigationManager>(() => NavigationManager(), fenix: true);
Get.lazyPut<LocationManager>(() => LocationManager(), fenix: true); Get.lazyPut<LocationManager>(() => LocationManager(), fenix: true);
// 火山AI服务 // 火山AI服务
// Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true); Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 账号管理服务 // 账号管理服务
Get.lazyPut<AccountService>(() => AccountService(), fenix: true); Get.lazyPut<AccountService>(() => AccountService(), fenix: true);

95
lib/core/themes/language/lang/en_us.dart

@ -1,95 +0,0 @@
const Map<String, String> enUS = {
"bottomNavHome": "home",
"bottomNavSleep": "sleep",
"bottomNavProfile": "profile",
"requestCancellation": "Request cancellation",
"connectionTimeout": "Connection timeout",
"requestTimeout": "Request timeout",
"responseTimeout": "Response timeout",
"unknownError": "Unknown error",
"welcomeHint": "DeepSound makes communication smoother",
"loginTitle": "Login/Register",
"contactCustomerService": "Connection",
"phoneHint": "Please enter your phone number",
"phoneError": "Wrong phone number!",
"emailHint": "Please enter email",
"emailError": "Email input error, please re-enter!",
"codeHint": "Please enter the verification code",
"codeError": "Verification code error",
"pleaseAgreeToTerms":
"Please check the box to agree to our Terms and Privacy Policy",
"getCode": "Get code",
"reacquireCode": "S Reacquire",
"bind": "Deterministic binding",
"login": "Login",
"otherLogin": "Other login methods",
"readAndAgree": "I have read and agree",
"serviceAgreement": " User Service Agreement ",
"and": "And",
"privacyAgreement": " User Privacy Agreement ",
"codeSend": "Verification code has been sent, please check email!",
"connectHeadphones": "Connect headphones",
"bindHeadphones": "Bind headphones",
"intelligentTool": "Intelligent tool",
"sessionMode": "AI conversation mode",
"translation": "Simultaneous translation",
"together": "Face-to-face translation",
"telephoneVoice": "Telephone translation",
"translationRecord": "Translation record",
"bindDevice": "Bind device",
"bound": "Bound",
"appVersion": "APP version",
"languageSetting": "Language setting",
"followingSystem": "Following system",
"userAgreement": "User agreement",
"privacyDeal": "Privacy agreement",
"chinese": "Chinese",
"english": "English",
"chineseComplex": "Chinese (Traditional)",
"japanese": "Japanese",
"korean": "Korean",
"russian": "Russian",
"french": "French",
"qrCode": "Authorization code or QR code",
"link": "Link",
"copy": "Copy",
"successfulReplication": "Successful replication",
"edit": "Edit",
"delete": "Delete",
"aiDialogue": "AI dialogue",
"initialChatMessage":
"Hi, I'm your new friend! Nice to meet you for the first time. I can answer your various questions, provide help with your work and studies, and even chat with you anytime. So, what would you like to ask?",
"defaultContent1": "Help me write a Python web scraper code",
"defaultContent2": "Help me plan a short video script",
"defaultContent3": "Help me design a mind-exercising game",
"defaultContent4": "Recommend a cost-effective phone",
"defaultContent5": "Come up with a creative name for my Honor of Kings ID",
"defaultContent6":
"Guide me on using Python to scrape real-time weather data",
"defaultContent7": "Tell me daily habits to boost kidney qi",
"defaultContent8": "Help me understand the basics of CNC programming",
"defaultContent9": "Help me find a short folk story",
"defaultContent10": "Recommend a cost-effective SUV",
"defaultContent11": "Teach me to make a small game using Python",
"defaultContent12": "Introduce some free-to-play mobile games",
"holdToSpeak": "Hold to speak",
"tapToSpeak": "Click to talk",
"hangUp": "Hang up",
"holdToStartSpeak": "Hold down and start talking",
"tapToStartSpeak": "Click to start Talking",
"sendMessage": "Ask me anything you want",
"beTalking": "Be talking ...",
"headphonesBeTalking": "Talking through headphones...",
"interlanguage": "Cannot choose the same language",
"notice": "Look out",
"connectingDevice": "Please set up the connected device first",
"bindDeviceTips": "Unbind the device first",
"cancel": "Cancel",
"confirm": "Confirm",
"goToSettings": "Go to Settings",
"goToBind": "Unbind",
"scanQrCode": "Scan the QR code",
"endRecording": "Do you want to end the recording?",
"earphone": "Earphone",
"voicerecorder": "VoiceRecorder"
};

93
lib/core/themes/language/lang/zh_cn.dart

@ -1,93 +0,0 @@
const Map<String, String> zhCN = {
"bottomNavHome": "首页",
"bottomNavSleep": "睡眠",
"bottomNavProfile": "我的",
"requestCancellation": "请求取消",
"connectionTimeout": "连接超时",
"requestTimeout": "请求超时",
"responseTimeout": "响应超时",
"unknownError": "未知错误",
"welcomeHint": "DeepSound让沟通更顺畅",
"loginTitle": "登录/注册账号",
"contactCustomerService": "联系客服",
"phoneHint": "请输入手机号",
"phoneError": "手机号码错误!",
"emailHint": "请输入邮箱",
"emailError": "邮箱输入错误,请重新输入!",
"codeHint": "请输入验证码",
"codeError": "验证码错误",
"pleaseAgreeToTerms": "请勾选以同意用户协议和隐私政策",
"getCode": "获取验证码",
"reacquireCode": "S后重新获取",
"bind": "确定绑定",
"login": "登录",
"otherLogin": "其他登录方式",
"readAndAgree": "我已阅读并同意",
"serviceAgreement": "《用户服务协议》",
"and": "和",
"privacyAgreement": "《用户隐私协议》",
"codeSend": "验证码已发送,请到邮箱查看!",
"connectHeadphones": "请连接耳机",
"bindHeadphones": "请绑定耳机",
"intelligentTool": "智能工具",
"sessionMode": "AI对话模式",
"translation": "同声传译",
"together": "面对面翻译",
"telephoneVoice": "通话语音翻译",
"translationRecord": "翻译记录",
"bindDevice": "绑定设备",
"bound": "已绑定",
"appVersion": "APP版本",
"languageSetting": "语言设置",
"followingSystem": "跟随系统",
"userAgreement": "用户协议",
"privacyDeal": "隐私协议",
"chinese": "中文",
"english": "英语",
"chineseComplex": "中文(繁体)",
"japanese": "日语",
"korean": "韩语",
"russian": "俄语",
"french": "法语",
"qrCode": "授权码或二维码",
"link": "链接",
"copy": "复制",
"successfulReplication": "复制成功",
"edit": "编辑",
"delete": "删除",
"aiDialogue": "AI对话",
"initialChatMessage":
"嗨,我是你的新朋友!初次见面很开心。我呢,可以回答你的各种问题,给你工作学习上提供帮助,还能随时陪你聊天。嗯,你想问点什么呢?",
"defaultContent1": "请帮我写个Python爬虫代码",
"defaultContent2": "帮我策划一个短视频脚本",
"defaultContent3": "请帮我设计一个锻炼思维的小游戏",
"defaultContent4": "请推荐一部性价比高的手机",
"defaultContent5": "帮我想一个王者ID的创意名称",
"defaultContent6": "请指导我使用Python爬取实时天气数据",
"defaultContent7": "告诉我补肾气的日常习惯",
"defaultContent8": "帮我了解CNC编程的基础知识",
"defaultContent9": "帮我找一个简短的民间故事",
"defaultContent10": "给我推荐一辆性价比高的SUV",
"defaultContent11": "教我用Python制作小游戏",
"defaultContent12": "介绍一些适合零氪玩家的手游",
"holdToSpeak": "按住说话",
"tapToSpeak": "点击说话",
"hangUp": "挂断",
"holdToStartSpeak": "按住开始说话",
"tapToStartSpeak": "点击开始说话",
"sendMessage": "有什么问的尽管问我",
"beTalking": "正在说话...",
"headphonesBeTalking": "正在使用耳机说话...",
"interlanguage": "不能选择同语种",
"notice": "注意",
"connectingDevice": "请先去设置连接设备",
"bindDeviceTips": "请先去绑定设备",
"cancel": "取消",
"confirm": "确定",
"goToSettings": "去设置",
"goToBind": "去绑定",
"scanQrCode": "扫描二维码",
"endRecording": "你想结束录音吗?",
"earphone": "耳机",
"voicerecorder": "录音笔"
};

18
lib/core/themes/language/translation_service.dart

@ -1,18 +0,0 @@
import 'dart:ui';
import 'package:get/get.dart';
import 'lang/en_us.dart';
import 'lang/zh_cn.dart';
class TranslationService extends Translations {
static Locale? get locale => Get.deviceLocale;
static final fallbackLocale = Locale('zh', 'CN');
static final supportedLocales = [
const Locale('zh', 'CN'),
const Locale('en', 'US'),
];
@override
Map<String, Map<String, String>> get keys => {'en_US': enUS, 'zh_CN': zhCN};
}

78
lib/core/translations/language/zh_cn.dart

@ -364,11 +364,85 @@ const Map<String, String> zhCN = {
"healthAdvisorWelcome": "您好,我是吴小江,很高兴能为您提供多功能AI聊天。我将用专业知识帮助您。请问有什么问题想咨询吗?", "healthAdvisorWelcome": "您好,我是吴小江,很高兴能为您提供多功能AI聊天。我将用专业知识帮助您。请问有什么问题想咨询吗?",
"healthAdvisorVoiceWelcome": "我在这里!", "healthAdvisorVoiceWelcome": "我在这里!",
// 翻译历史 // 翻译模块
// 翻译主页 (translation_view.dart)
"translationHistory": "翻译历史",
"enableRecording": "是否开启录音",
"viewAudioFiles": "查看录音文件",
"headsetUser": "耳机用户",
"phoneUser": "手机用户",
// 翻译历史页 (translation_history_view.dart)
"noTranslationHistory": "暂无历史记录", "noTranslationHistory": "暂无历史记录",
"clearHistory": "清除历史记录", "clearHistory": "清除历史记录",
"clearHistoryConfirm": "确定要清除所有翻译历史记录吗?此操作无法撤销。", "clearHistoryConfirm": "确定要清除所有翻译历史记录吗?此操作无法撤销。",
"translationHistory": "翻译历史", "unknownLanguage": "未知语言",
"deleteSessionRecord": "删除会话记录",
"deleteSessionConfirm": "确定要删除该会话的所有翻译记录吗?此操作无法撤销。",
// 翻译录音文件列表页 (translation_list_view.dart)
"translateAudioFiles": "翻译录音文件",
"noAudioFiles": "暂无音频文件",
"calculating": "计算中...",
"playing": "播放中...",
"paused": "已暂停",
"clickToPlay": "点击播放",
"rename": "重命名",
"smartTranscription": "智能转写",
"renameAudioFile": "重命名音频文件",
"enterNewFileName": "请输入新的文件名",
// 翻译详情页 (translation_detail_view.dart)
"originalRecord": "原始记录",
"recordSummary": "记录摘要",
"clickToGenerateSummary": "点击下方按钮生成会话摘要",
"summaryContent": "摘要内容",
"copied": "已复制",
"summaryCopiledToClipboard": "摘要已复制到剪贴板",
"extractSummary": "提取摘要",
"summaryGenerated": "已生成摘要",
"generating": "生成中...",
"regenerate": "重新生成",
"summaryTimeoutMessage": "生成摘要请求超时,可能是网络问题或服务器繁忙。",
"deleteTranslationRecord": "删除翻译记录",
"deleteTranslationConfirm": "确定要删除这条翻译记录吗?此操作无法撤销。",
"originalText": "原文",
"translatedText": "译文",
"sessionSummary": "会话摘要",
"cannotShare": "无法分享",
"generateSummaryFirst": "请先生成摘要",
"translationSessionRecord": "翻译会话记录",
// 配对模块
// 配对页面 (pairing_view.dart & pairing_view_new.dart)
"devicePairing": "设备配对",
"connectYourDeepSoundHeadset": "连接您的 DeepSound 耳机",
"bind": "绑定",
"authorizeBind": "授权绑定",
"enterCode": "请输入代码",
"autoInputCode": "自动输入代码",
"skipBinding": "跳过绑定",
"rescan": "重新扫描",
"skip": "跳过",
// 配对状态文本
"readyToPair": "准备开始配对",
"preparingPairingEnvironment": "正在准备配对环境...",
"connectDeepSoundViaSystemBluetooth": "请先通过系统蓝牙连接DeapSound设备",
"verifyingDevice": "正在验证设备...",
"deviceBindingConfirm": "设备绑定确认...",
"establishingDeviceAssociation": "正在建立设备关联...",
"waitingCompletePairing": "请稍候,正在完成配对...",
"pairingSuccessRedirecting": "配对成功!即将跳转到主页...",
// 配对错误状态文本
"ensureConnectedDeepSoundDevice": "请确保连接的是DeapSound设备",
"deviceAlreadyBoundToOtherAccount": "该设备已被其他账号绑定",
"deviceAssociationFailed": "设备关联失败,请重试",
"scanTimeoutEnsureDeviceNearby": "扫描超时,请确保设备在附近",
"networkError": "网络错误",
"authorizationFailedCheckCode": "授权失败,请检查授权码是否正确",
"pairingFailedRetry": "配对失败,请重试",
//用户反馈页 //用户反馈页
"feedback": "用户反馈", "feedback": "用户反馈",

38
lib/data/services/ble_manager.dart

@ -472,13 +472,37 @@ class BleManager extends GetxService {
firmwareProductColor: '', firmwareProductColor: '',
); );
} }
final commands = [
// 并行获取所有设备信息,不使用延迟 _bleService.getBatteryInfo,
final results = await Future.wait([ _bleService.getDeviceVersion,
_bleService.getBatteryInfo(), _bleService.getProductId,
_bleService.getDeviceVersion(), ];
_bleService.getProductId(),
]); final delays = [
Duration.zero, // 第一个命令立即执行
const Duration(milliseconds: 100), // 第二个命令延迟100ms
const Duration(milliseconds: 100), // 第三个命令延迟200ms
];
final futures = commands
.asMap()
.map((index, command) {
final delay = delays[index];
return MapEntry(
index,
Future.delayed(delay, command),
);
})
.values
.toList();
final results = await Future.wait(futures);
// // 并行获取所有设备信息,不使用延迟
// final results = await Future.wait([
// _bleService.getBatteryInfo(),
// _bleService.getDeviceVersion(),
// _bleService.getProductId(),
// ]);
Logger.info( Logger.info(
'设备信息获取结果: 电池=${results[0]}, 版本=${results[1]}, 产品ID=${results[2]}'); '设备信息获取结果: 电池=${results[0]}, 版本=${results[1]}, 产品ID=${results[2]}');

4
lib/data/services/music_manager.dart

@ -105,7 +105,7 @@ class MusicManager extends GetxService {
required Function() onPrevious, required Function() onPrevious,
required Function(int) onSeekTo, required Function(int) onSeekTo,
required Function() onStop, required Function() onStop,
required Function(String, int, int, double) onStop1, required Function(int, int, int, double) onProgress,
}) async { }) async {
try { try {
await _musicService.setMediaSessionCallbacks( await _musicService.setMediaSessionCallbacks(
@ -115,7 +115,7 @@ class MusicManager extends GetxService {
onPrevious: onPrevious, onPrevious: onPrevious,
onSeekTo: onSeekTo, onSeekTo: onSeekTo,
onStop: onStop, onStop: onStop,
onStop1: onStop1, onProgress: onProgress,
); );
} catch (e) { } catch (e) {
Logger.d('MusicManager', '设置媒体会话回调失败: $e'); Logger.d('MusicManager', '设置媒体会话回调失败: $e');

318
lib/modules/agent/controllers/agent_controller.dart

@ -5,7 +5,6 @@ import 'dart:io';
import 'package:amap_service/amap_service.dart'; import 'package:amap_service/amap_service.dart';
import 'package:deep_voice/core/utils/upload_oss.dart'; import 'package:deep_voice/core/utils/upload_oss.dart';
import 'package:deep_voice/data/models/user_Info.dart'; import 'package:deep_voice/data/models/user_Info.dart';
import 'package:deep_voice/data/services/location_manager.dart';
import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/data/services/music_manager.dart';
import 'package:deep_voice/data/services/navigation_manager.dart'; import 'package:deep_voice/data/services/navigation_manager.dart';
import 'package:deep_voice/data/services/network/api.dart'; import 'package:deep_voice/data/services/network/api.dart';
@ -121,16 +120,23 @@ class AgentController extends GetxController {
// 会话ID // 会话ID
final String sessionId = 'default_agent'; final String sessionId = 'default_agent';
// 分页加载相关状态
final isLoadingMore = false.obs; // 是否正在加载更多消息
final hasMoreMessages = true.obs; // 是否还有更多消息
final currentPage = 1.obs; // 当前页码
final pageSize = 20; // 每页消息数量
final isLoadingMoreError = false.obs; // 加载更多时是否出错
// 是否已加载聊天记录 // 是否已加载聊天记录
final isHistoryLoaded = false.obs; final isHistoryLoaded = false.obs;
// 添加加载状态跟踪 // 添加加载状态跟踪
final RxBool isLoadingMusic = false.obs; final RxBool isLoadingMusic = false.obs;
final RxString loadingMusicId = ''.obs; final RxInt loadingMusicId = 0.obs;
// 设置加载状态的方法 // 设置加载状态的方法
void setLoadingMusic(String id, bool loading) { void setLoadingMusic(int id, bool loading) {
loadingMusicId.value = loading ? id : ''; loadingMusicId.value = loading ? id : 0;
isLoadingMusic.value = loading; isLoadingMusic.value = loading;
} }
@ -138,6 +144,7 @@ class AgentController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
_loadTtsPreference(); //加载TTS偏好设置 _loadTtsPreference(); //加载TTS偏好设置
_setupScrollListener(); //设置滚动监听器
_loadChatHistory(); //加载聊天历史记录 _loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件 _subscribeToEvents(); //订阅代理服务事件
_initMusicStatus(); //初始化音乐状态 _initMusicStatus(); //初始化音乐状态
@ -166,7 +173,7 @@ class AgentController extends GetxController {
onPrevious: onMediaSessionPrevious, onPrevious: onMediaSessionPrevious,
onSeekTo: onMediaSessionSeekTo, onSeekTo: onMediaSessionSeekTo,
onStop: onMediaSessionStop, onStop: onMediaSessionStop,
onStop1: onMediaSessionSeekTo1, onProgress: onMediaSessionProgress,
); );
Logger.d('AgentController', '媒体会话回调设置完成'); Logger.d('AgentController', '媒体会话回调设置完成');
} catch (e) { } catch (e) {
@ -271,63 +278,33 @@ class AgentController extends GetxController {
final result = await ChatStorage.getMessages( final result = await ChatStorage.getMessages(
sessionId: sessionId, //会话ID sessionId: sessionId, //会话ID
page: 1, //从第一页开始 page: 1, //从第一页开始
pageSize: 20, //每页20条记录 pageSize: pageSize, //每页20条记录
); );
if (result.messages.isNotEmpty) { if (result.messages.isNotEmpty) {
// 转换并添加消息到列表 // 转换并添加消息到列表
final historyMessages = result.messages.map((msg) { final historyMessages = _convertChatMessagesToMessages(result.messages);
final meta = msg.metadata != null && msg.metadata != ""
? jsonDecode(msg.metadata!)
: null; // 如果为空,使用空对象
// 从元数据中提取图片信息
final hasImage = meta != null && meta.containsKey('hasImage')
? meta['hasImage'] as bool
: false;
final imagePath = meta != null && meta.containsKey('imagePath')
? meta['imagePath'] as String?
: null;
return Message(
isUser: msg.sender == 'user',
text: msg.message,
meta: meta,
timestamp: DateTime.parse(msg.timestamp),
hasImage: hasImage,
imagePath: imagePath,
);
}).toList();
// 消息按时间排序,最新的消息在底部 // 消息按时间排序,最新的消息在底部
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messages.assignAll(historyMessages); messages.assignAll(historyMessages);
Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录'); // 更新分页状态
// 详细打印每条消息的内容和属性,便于调试 currentPage.value = 1;
Logger.d(TAG, '===== 历史消息详情开始 ====='); hasMoreMessages.value = result.totalPages > 1;
for (int i = 0; i < historyMessages.length; i++) {
final msg = historyMessages[i]; Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}');
Logger.d( _logMessageDetails(historyMessages);
TAG,
'消息[$i]: {'
'isUser: ${msg.isUser}, '
'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", '
'time: ${msg.timestamp}, '
'hasImage: ${msg.hasImage}, '
'imagePath: ${msg.imagePath ?? "无"}, '
'meta: ${msg.meta != null ? "有" : "无"}'
'}');
}
Logger.d(TAG, '===== 历史消息详情结束 =====');
} else { } else {
// 没有历史记录时添加欢迎消息 // 没有历史记录时添加欢迎消息
_addWelcomeMessage(); _addWelcomeMessage();
hasMoreMessages.value = false;
Logger.i(TAG, '没有历史记录,添加欢迎消息'); Logger.i(TAG, '没有历史记录,添加欢迎消息');
} }
} catch (e) { } catch (e) {
Logger.e(TAG, '加载聊天历史失败: $e'); Logger.e(TAG, '加载聊天历史失败: $e');
_addWelcomeMessage(); _addWelcomeMessage();
hasMoreMessages.value = false;
} finally { } finally {
isHistoryLoaded.value = true; isHistoryLoaded.value = true;
messages.refresh(); messages.refresh();
@ -335,6 +312,137 @@ class AgentController extends GetxController {
} }
} }
// 加载更多历史消息(分页加载)
Future<void> loadMoreMessages() async {
// 防止重复加载
if (isLoadingMore.value || !hasMoreMessages.value) {
Logger.d(TAG, '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreMessages=${hasMoreMessages.value}');
return;
}
try {
isLoadingMore.value = true;
isLoadingMoreError.value = false;
final nextPage = currentPage.value + 1;
Logger.i(TAG, '正在加载第 $nextPage 页历史消息...');
final result = await ChatStorage.getMessages(
sessionId: sessionId,
page: nextPage,
pageSize: pageSize,
);
if (result.messages.isNotEmpty) {
// 转换新消息
final newMessages = _convertChatMessagesToMessages(result.messages);
// 按时间排序新消息
newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
// 添加短暂延迟,让用户感知到加载过程
await Future.delayed(const Duration(milliseconds: 300));
// 将新消息插入到现有消息列表的开头(因为是历史消息)
final updatedMessages = [...newMessages, ...messages];
messages.assignAll(updatedMessages);
// 更新分页状态
currentPage.value = nextPage;
hasMoreMessages.value = nextPage < result.totalPages;
Logger.i(TAG, '成功加载 ${newMessages.length} 条历史消息,当前页: $nextPage/${result.totalPages}');
} else {
// 没有更多消息
hasMoreMessages.value = false;
Logger.i(TAG, '没有更多历史消息');
}
} catch (e) {
Logger.e(TAG, '加载更多消息失败: $e');
isLoadingMoreError.value = true;
} finally {
isLoadingMore.value = false;
}
}
// 转换ChatMessage到Message的通用方法
List<Message> _convertChatMessagesToMessages(List<ChatMessage> chatMessages) {
return chatMessages.map((msg) {
final meta = msg.metadata != null && msg.metadata != ""
? jsonDecode(msg.metadata!)
: null; // 如果为空,使用空对象
// 从元数据中提取图片信息
final hasImage = meta != null && meta.containsKey('hasImage')
? meta['hasImage'] as bool
: false;
final imagePath = meta != null && meta.containsKey('imagePath')
? meta['imagePath'] as String?
: null;
return Message(
isUser: msg.sender == 'user',
text: msg.message,
meta: meta,
timestamp: DateTime.parse(msg.timestamp),
hasImage: hasImage,
imagePath: imagePath,
);
}).toList();
}
// 打印消息详情的通用方法
void _logMessageDetails(List<Message> messageList) {
Logger.d(TAG, '===== 历史消息详情开始 =====');
for (int i = 0; i < messageList.length; i++) {
final msg = messageList[i];
Logger.d(
TAG,
'消息[$i]: {'
'isUser: ${msg.isUser}, '
'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", '
'time: ${msg.timestamp}, '
'hasImage: ${msg.hasImage}, '
'imagePath: ${msg.imagePath ?? "无"}, '
'meta: ${msg.meta != null ? "有" : "无"}'
'}');
}
Logger.d(TAG, '===== 历史消息详情结束 =====');
}
// 设置滚动监听器
void _setupScrollListener() {
scrollController.addListener(() {
// 检查是否滚动到顶部(因为使用了reverse: true,顶部实际是最大滚动位置)
if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) {
// 当距离顶部100像素时触发加载更多
if (hasMoreMessages.value && !isLoadingMore.value) {
Logger.d(TAG, '触发加载更多历史消息');
loadMoreMessages();
}
}
});
}
// 手动触发加载更多(用于测试或手动调用)
void triggerLoadMore() {
if (hasMoreMessages.value && !isLoadingMore.value) {
Logger.i(TAG, '手动触发加载更多历史消息');
loadMoreMessages();
} else {
Logger.w(TAG, '无法加载更多:hasMoreMessages=${hasMoreMessages.value}, isLoadingMore=${isLoadingMore.value}');
}
}
// 重置分页状态(用于刷新聊天记录)
void resetPagination() {
currentPage.value = 1;
hasMoreMessages.value = true;
isLoadingMore.value = false;
isLoadingMoreError.value = false;
Logger.i(TAG, '分页状态已重置');
}
// 添加初始欢迎消息 // 添加初始欢迎消息
void _addWelcomeMessage() { void _addWelcomeMessage() {
messages.add(Message( messages.add(Message(
@ -1308,7 +1416,6 @@ class AgentController extends GetxController {
// 设置按住说话状态 // 设置按住说话状态
isPushToTalkActive.value = true; isPushToTalkActive.value = true;
print("开始按住说话,状态已设置为true");
// 开始语音识别 // 开始语音识别
await AgentService.audioContinue(); await AgentService.audioContinue();
// await startVoiceInput(); // await startVoiceInput();
@ -1333,7 +1440,7 @@ class AgentController extends GetxController {
try { try {
var status = await _musiceManager.getStatus(); var status = await _musiceManager.getStatus();
isPlaying.value = status['isPlaying'] ?? false; isPlaying.value = status['isPlaying'] ?? false;
currentId.value = status['id'] ?? 0; currentId.value = status['id'] as int;
Logger.d('AgentController', Logger.d('AgentController',
'音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}');
print("音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}"); print("音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}");
@ -1351,16 +1458,14 @@ class AgentController extends GetxController {
isPlaying.value = true; isPlaying.value = true;
// 修复:确保currentId接收int类型 // 修复:确保currentId接收int类型
final songId = song['id']; final songId = song['id'];
currentId.value = songId is int currentId.value = songId ?? 0;
? songId
: (int.tryParse(songId?.toString() ?? '0') ?? 0);
// 重置进度状态 // 重置进度状态
currentPosition.value = 0; currentPosition.value = 0;
duration.value = 0; duration.value = 0;
progress.value = 0.0; progress.value = 0.0;
final id = song['id'] ?? ""; final id = song['id'] ?? 0;
final timestampMs = song['ts'] ?? 0; final timestampMs = song['ts'] ?? 0;
// 检查URL是否需要刷新(只有当有时间戳时才检查) // 检查URL是否需要刷新(只有当有时间戳时才检查)
@ -1382,9 +1487,7 @@ class AgentController extends GetxController {
song["url"] = urldata['url'] ?? ''; song["url"] = urldata['url'] ?? '';
// 修复:确保currentId接收int类型 // 修复:确保currentId接收int类型
final newSongId = song['id']; final newSongId = song['id'];
currentId.value = newSongId is int currentId.value = newSongId ?? 0;
? newSongId
: (int.tryParse(newSongId?.toString() ?? '0') ?? 0);
Logger.d(TAG, '获取新URL成功'); Logger.d(TAG, '获取新URL成功');
} catch (e) { } catch (e) {
Logger.e(TAG, '获取新URL失败: $e'); Logger.e(TAG, '获取新URL失败: $e');
@ -1394,8 +1497,8 @@ class AgentController extends GetxController {
} }
// 转换为MusicManager期望的格式 // 转换为MusicManager期望的格式
final musicData = <String, String>{ final musicData = <String, dynamic>{
'id': song['id']?.toString() ?? '', 'id': song['id'],
'url': song['url']?.toString() ?? '', 'url': song['url']?.toString() ?? '',
'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'title': song['title']?.toString() ?? song['name']?.toString() ?? '',
'artist': 'artist':
@ -1411,9 +1514,6 @@ class AgentController extends GetxController {
await _updateMediaSessionMetadata(musicData); await _updateMediaSessionMetadata(musicData);
await _updateMediaSessionPlaybackState(true); await _updateMediaSessionPlaybackState(true);
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐播放启动成功'); Logger.d(TAG, '音乐播放启动成功');
} catch (e) { } catch (e) {
Logger.e(TAG, '播放音乐失败: $e'); Logger.e(TAG, '播放音乐失败: $e');
@ -1444,7 +1544,6 @@ class AgentController extends GetxController {
Logger.d('AgentController', '恢复播放音乐'); Logger.d('AgentController', '恢复播放音乐');
await _musiceManager.resume(); await _musiceManager.resume();
isPlaying.value = true; isPlaying.value = true;
_startProgressTimer();
// 更新媒体会话播放状态 // 更新媒体会话播放状态
await _updateMediaSessionPlaybackState(true); await _updateMediaSessionPlaybackState(true);
@ -1456,23 +1555,6 @@ class AgentController extends GetxController {
} }
} }
// 启动进度更新定时器
void _startProgressTimer() {
_stopProgressTimer(); // 先停止现有定时器
Logger.d('AgentController', '启动播放进度定时器');
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (isPlaying.value && currentId.value != 0) {
updateMusicProgress();
} else if (!isPlaying.value) {
// 如果不在播放状态,停止定时器
timer.cancel();
_progressTimer = null;
Logger.d('AgentController', '播放已停止,取消进度定时器');
}
});
}
// 停止进度更新定时器 // 停止进度更新定时器
void _stopProgressTimer() { void _stopProgressTimer() {
if (_progressTimer != null) { if (_progressTimer != null) {
@ -1483,61 +1565,17 @@ class AgentController extends GetxController {
} }
// 获取播放进度 // 获取播放进度
Future<void> updateMusicProgress() async { Future<void> updateMusicProgress(int currentIdInternal, int newPosition,
try {
if (!isPlaying.value || currentId.value == 0) {
return; // 如果没有在播放,直接返回
}
// var progressInfo = await _musiceManager.getProgress();
// final newPosition = progressInfo['currentPosition'] ?? 0;
// final newDuration = progressInfo['duration'] ?? 0;
// final newProgress = progressInfo['progress'] ?? 0.0;
// bool hasUpdate = false;
// // 只有当值发生变化时才更新,避免不必要的UI刷新
// if (currentPosition.value != newPosition) {
// currentPosition.value = newPosition;
// hasUpdate = true;
// }
// if (duration.value != newDuration) {
// duration.value = newDuration;
// hasUpdate = true;
// }
// if ((progress.value - newProgress).abs() > 0.001) {
// // 避免浮点数精度问题
// progress.value = newProgress;
// hasUpdate = true;
// }
// 如果有进度更新,通知系统媒体会话更新进度信息
// if (hasUpdate) {
// await _updateMediaSessionProgress();
// }
} catch (e) {
Logger.e('AgentController', '获取播放进度失败: $e');
}
}
// 获取播放进度
Future<void> updateMusicProgress1(String currentIdInternal, int newPosition,
int newDuration, double newProgress) async { int newDuration, double newProgress) async {
try { try {
if (int.parse(currentIdInternal) != 0) { if (currentIdInternal != 0) {
isPlaying.value = true; isPlaying.value = true;
// return; // 如果没有在播放,直接返回 // return; // 如果没有在播放,直接返回
} }
// // var progressInfo = await _musiceManager.getProgress();
// final newPosition = progressInfo['currentPosition'] ?? 0;
// final newDuration = progressInfo['duration'] ?? 0;
// final newProgress = progressInfo['progress'] ?? 0.0;
print(
"updateMusicProgress1:currentIdInternal:$currentIdInternal newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress");
bool hasUpdate = false; bool hasUpdate = false;
if (currentId.value != int.parse(currentIdInternal)) { if (currentId.value != currentIdInternal) {
currentId.value = int.parse(currentIdInternal); currentId.value = currentIdInternal;
hasUpdate = true; hasUpdate = true;
} }
// 只有当值发生变化时才更新,避免不必要的UI刷新 // 只有当值发生变化时才更新,避免不必要的UI刷新
@ -1602,20 +1640,17 @@ class AgentController extends GetxController {
Future<void> seekToPosition(int positionMs) async { Future<void> seekToPosition(int positionMs) async {
try { try {
await _musiceManager.seekTo(positionMs); await _musiceManager.seekTo(positionMs);
// 立即更新进度状态
// await updateMusicProgress();
} catch (e) { } catch (e) {
Logger.e('AgentController', '跳转播放位置失败: $e'); Logger.e('AgentController', '跳转播放位置失败: $e');
} }
} }
// 跳转到指定位置 // 跳转到指定位置
Future<void> seekToPosition1(String currentIdInternal, int newPosition, Future<void> seekToProgress(int currentIdInternal, int newPosition,
int newDuration, double newProgress) async { int newDuration, double newProgress) async {
try { try {
//await _musiceManager.seekTo(positionMs);
// 立即更新进度状态 // 立即更新进度状态
await updateMusicProgress1( await updateMusicProgress(
currentIdInternal, newPosition, newDuration, newProgress); currentIdInternal, newPosition, newDuration, newProgress);
} catch (e) { } catch (e) {
Logger.e('AgentController', '跳转播放位置失败: $e'); Logger.e('AgentController', '跳转播放位置失败: $e');
@ -1676,7 +1711,7 @@ class AgentController extends GetxController {
// 设置加载状态 // 设置加载状态
isLoadingMusic.value = true; isLoadingMusic.value = true;
loadingMusicId.value = playlist[0]['id']?.toString() ?? ''; loadingMusicId.value = playlist[0]['id'];
// 重置播放状态 // 重置播放状态
isPlaying.value = true; isPlaying.value = true;
@ -1687,7 +1722,7 @@ class AgentController extends GetxController {
// 格式化歌曲列表数据 // 格式化歌曲列表数据
final formattedPlaylist = playlist.map((song) { final formattedPlaylist = playlist.map((song) {
return { return {
'id': song['id']?.toString() ?? '', 'id': song['id'],
'url': song['url']?.toString() ?? '', 'url': song['url']?.toString() ?? '',
'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'title': song['title']?.toString() ?? song['name']?.toString() ?? '',
'artist': 'artist':
@ -1702,12 +1737,9 @@ class AgentController extends GetxController {
// 更新当前播放歌曲ID // 更新当前播放歌曲ID
if (formattedPlaylist.isNotEmpty) { if (formattedPlaylist.isNotEmpty) {
currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0; currentId.value = formattedPlaylist[0]['id'] ?? 0;
} }
// 启动进度定时器
_startProgressTimer();
Logger.d(TAG, '音乐列表播放启动成功'); Logger.d(TAG, '音乐列表播放启动成功');
} catch (e) { } catch (e) {
Logger.e(TAG, '播放音乐列表失败: $e'); Logger.e(TAG, '播放音乐列表失败: $e');
@ -1726,7 +1758,7 @@ class AgentController extends GetxController {
} finally { } finally {
// 清除加载状态 // 清除加载状态
isLoadingMusic.value = false; isLoadingMusic.value = false;
loadingMusicId.value = ''; loadingMusicId.value = 0;
} }
} }
@ -1778,7 +1810,7 @@ class AgentController extends GetxController {
// 查找当前歌曲在列表中的索引 // 查找当前歌曲在列表中的索引
int index = -1; int index = -1;
for (int i = 0; i < playlist.length; i++) { for (int i = 0; i < playlist.length; i++) {
if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) { if (playlist[i]['id'] == songId) {
index = i; index = i;
break; break;
} }
@ -1836,12 +1868,12 @@ class AgentController extends GetxController {
_stopProgressTimer(); _stopProgressTimer();
} }
/// 处理来自系统通知栏的跳转操作 /// 处理来自系统进度条
Future<void> onMediaSessionSeekTo1(String currentIdInternal, int newPosition, Future<void> onMediaSessionProgress(int currentIdInternal, int newPosition,
int newDuration, double newProgress) async { int newDuration, double newProgress) async {
print( // print(
"onMediaSessionSeekTo1 currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); // "onMediaSessionProgress currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress");
await seekToPosition1( await seekToProgress(
currentIdInternal, newPosition, newDuration, newProgress); currentIdInternal, newPosition, newDuration, newProgress);
} }
} }

83
lib/modules/agent/views/agent_view.dart

@ -200,8 +200,13 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
reverse: true, reverse: true,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 16, horizontal: 16), vertical: 16, horizontal: 16),
itemCount: controller.messages.length, itemCount: controller.messages.length + (controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
// 如果是最后一项且有更多消息,显示加载指示器
if (index == controller.messages.length && controller.hasMoreMessages.value) {
return _buildLoadMoreIndicator();
}
final message = controller final message = controller
.messages[controller.messages.length - 1 - index]; .messages[controller.messages.length - 1 - index];
return Padding( return Padding(
@ -1122,4 +1127,80 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
), ),
); );
} }
// 构建加载更多指示器
Widget _buildLoadMoreIndicator() {
return Obx(() {
if (controller.isLoadingMore.value) {
// 正在加载状态
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: const Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 8),
Text(
'加载更多消息...',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
);
} else if (controller.isLoadingMoreError.value) {
// 加载失败状态
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: GestureDetector(
onTap: () => controller.loadMoreMessages(),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.refresh,
size: 16,
color: Colors.grey,
),
SizedBox(width: 8),
Text(
'加载失败,点击重试',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
),
);
} else {
// 默认状态(有更多消息但未加载)
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: const Center(
child: Text(
'向上滑动查看更多',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
),
);
}
});
}
} }

2
lib/modules/agent/views/music_card.dart

@ -200,7 +200,7 @@ Widget buildMusicContent(
...playlist.asMap().entries.take(4).map((entry) { ...playlist.asMap().entries.take(4).map((entry) {
final index = entry.key; final index = entry.key;
final item = entry.value; final item = entry.value;
final id = item["id"] ?? 0; final id = item["id"];
final name = item["name"] ?? "未知歌曲"; final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? ""; final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手"; final singer = item["sgener"] ?? "未知歌手";

4
lib/modules/agent/widgets/sound_bar_widget.dart

@ -134,8 +134,10 @@ class AgentSoundBarsWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
final controller = Get.find<AgentController>(); final controller = Get.find<AgentController>();
final shouldShowAnimation =
controller.isListening.value || controller.isPushToTalkActive.value;
return SoundBarsWidget( return SoundBarsWidget(
isActive: controller.isListening.value, isActive: shouldShowAnimation,
barCount: barCount, barCount: barCount,
); );
}); });

15
lib/modules/music/views/music_playlist_view.dart

@ -1,8 +1,6 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter/services.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../agent/controllers/agent_controller.dart'; import '../../agent/controllers/agent_controller.dart';
import '../../agent/views/circular_progress_button.dart'; import '../../agent/views/circular_progress_button.dart';
@ -80,12 +78,11 @@ class MusicPlaylistView extends StatelessWidget {
if (controller.currentId.value != 0) { if (controller.currentId.value != 0) {
try { try {
currentSong = playlist.firstWhere( currentSong = playlist.firstWhere(
(song) => (song) => song['id'] == controller.currentId.value,
int.tryParse(song['id']?.toString() ?? '0') ==
controller.currentId.value,
); );
} catch (e) { } catch (e) {
// 如果没找到,使用第一首歌曲 // 如果没找到,使用第一首歌曲
currentSong = playlist.isNotEmpty ? playlist.first : null; currentSong = playlist.isNotEmpty ? playlist.first : null;
} }
} else { } else {
@ -192,7 +189,7 @@ class MusicPlaylistView extends StatelessWidget {
itemCount: playlist.length, itemCount: playlist.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = playlist[index]; final item = playlist[index];
final id = item["id"]?.toString() ?? ""; final id = item["id"] ?? 0;
final name = item["name"] ?? "未知歌曲"; final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? ""; final url = item["url"] ?? "";
final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; final singer = item["singer"] ?? item["sgener"] ?? "未知歌手";
@ -215,7 +212,7 @@ class MusicPlaylistView extends StatelessWidget {
Widget _buildMusicItem( Widget _buildMusicItem(
BuildContext context, BuildContext context,
AgentController controller, AgentController controller,
String id, int id,
String name, String name,
String url, String url,
String singer, String singer,
@ -323,7 +320,7 @@ class MusicPlaylistView extends StatelessWidget {
SizedBox(width: 14.w), SizedBox(width: 14.w),
// 播放按钮 // 播放按钮
Obx(() { Obx(() {
final currentIdInt = int.tryParse(id) ?? 0; final currentIdInt = id;
final isThisPlaying = controller.isPlaying.value && final isThisPlaying = controller.isPlaying.value &&
controller.currentId.value == currentIdInt; controller.currentId.value == currentIdInt;
final currentProgress = final currentProgress =
@ -601,7 +598,7 @@ class MusicPlaylistView extends StatelessWidget {
// 播放选中的歌曲 // 播放选中的歌曲
void _playSelectedSong(AgentController controller, Map<String, dynamic> song, void _playSelectedSong(AgentController controller, Map<String, dynamic> song,
List<Map<String, dynamic>> playlist) { List<Map<String, dynamic>> playlist) {
final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0; final songId = song['id'] ?? 0;
controller.updateCurrentPlaylistContext(playlist, songId); controller.updateCurrentPlaylistContext(playlist, songId);
controller.musicPlay(song); controller.musicPlay(song);
} }

851
lib/modules/ota_upgrade/views/ota_upgrade_view.dart

@ -89,33 +89,17 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: EdgeInsets.all(20), padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// 设备信息卡片 // 设备信息卡片 - 优化后的版本
Card( _buildDeviceCard(isDarkMode),
elevation: 2, SizedBox(height: 24.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: isDarkMode ? Colors.grey[900] : Colors.white,
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
_buildDeviceIcon(isDarkMode),
SizedBox(height: 20),
_buildVersionInfo(isDarkMode),
],
),
),
),
SizedBox(height: 20),
// 更新说明 // 更新说明 - 当有可用更新时显示
_buildUpdateNotes(isDarkMode), //_buildUpdateNotes(isDarkMode),
SizedBox(height: 20), SizedBox(height: 16.h),
// 状态显示区域 // 状态显示区域
Obx(() { Obx(() {
@ -135,257 +119,696 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
); );
} }
// 设备信息卡片
Widget _buildDeviceCard(bool isDarkMode) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [
const Color(0xFF1A1A1A).withValues(alpha: 0.9),
const Color(0xFF2A2A2A).withValues(alpha: 0.9),
]
: [
const Color(0xFFF8FAFF),
const Color(0xFFE3F2FD),
],
),
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 0,
),
],
),
child: Padding(
padding: EdgeInsets.all(24.w),
child: Column(
children: [
// 设备图标区域
_buildDeviceIcon(isDarkMode),
SizedBox(height: 24.h),
// 版本信息区域
_buildVersionInfo(isDarkMode),
],
),
),
);
}
// 设备图标 // 设备图标
Widget _buildDeviceIcon(bool isDarkMode) => Image.asset( Widget _buildDeviceIcon(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.05)
: Colors.white.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: isDarkMode
? Colors.white.withValues(alpha: 0.1)
: Colors.blue.withValues(alpha: 0.1),
width: 1,
),
),
child: Image.asset(
isDarkMode isDarkMode
? 'assets/images/headphone_dark.png' ? 'assets/images/headphone_dark.png'
: 'assets/images/headphone_light.png', : 'assets/images/headphone_light.png',
height: 180.h, height: 120.h,
fit: BoxFit.contain, fit: BoxFit.contain,
); ),
);
}
Widget _buildVersionInfo(bool isDarkMode) => Column( // 版本信息显示
mainAxisAlignment: MainAxisAlignment.start, Widget _buildVersionInfo(bool isDarkMode) {
crossAxisAlignment: CrossAxisAlignment.start, return Column(
children: [
// 当前版本信息
_buildVersionInfoRow(
label: '当前固件版本'.tr,
version: controller.currentVersion,
isDarkMode: isDarkMode,
isLatest: false,
),
SizedBox(height: 16.h),
// 最新版本信息
_buildVersionInfoRow(
label: '最新固件版本'.tr,
version: controller.latestVersion,
isDarkMode: isDarkMode,
isLatest: true,
),
// 版本状态指示器
SizedBox(height: 20.h),
_buildVersionStatusIndicator(isDarkMode),
],
);
}
// 版本信息行组件
Widget _buildVersionInfoRow({
required String label,
required RxString version,
required bool isDarkMode,
required bool isLatest,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.03)
: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isDarkMode
? Colors.white.withValues(alpha: 0.08)
: Colors.grey.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Text(
crossAxisAlignment: CrossAxisAlignment.center, // 改为center对齐 label,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
Obx(() => Row(
children: [
if (isLatest && controller.isUpdateAvailable.value) ...[
Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8.r),
),
child: Text(
'NEW',
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
),
SizedBox(width: 8.w),
],
Text(
'V ${version.value}',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode
? (isLatest ? Colors.green[300] : Colors.blue[300])
: (isLatest ? Colors.green[600] : Colors.blue[600]),
),
),
],
)),
],
),
);
}
// 版本状态指示器
Widget _buildVersionStatusIndicator(bool isDarkMode) {
return Obx(() {
if (controller.isUpdateAvailable.value) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.orange.withValues(alpha: 0.1),
Colors.red.withValues(alpha: 0.1),
],
),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: Colors.orange.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('当前固件版本:'.tr, Icon(
style: TextStyle( Icons.update,
fontSize: 18, size: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black)), color: Colors.orange,
SizedBox(width: 8), // 改为width ),
Obx(() => Text('V ${controller.currentVersion}', SizedBox(width: 6.w),
style: TextStyle( Text(
fontSize: 24, '发现新版本',
fontWeight: FontWeight.normal, style: TextStyle(
color: isDarkMode ? Colors.blue[300] : Colors.blue))), fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Colors.orange,
),
),
], ],
), ),
SizedBox(height: 8), // 改为height );
Row( } else {
crossAxisAlignment: CrossAxisAlignment.center, // 改为center对齐 return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: Colors.green.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('最新固件版本:'.tr, Icon(
style: TextStyle( Icons.check_circle,
fontSize: 18, size: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black)), color: Colors.green,
SizedBox(width: 8), // 改为width ),
Obx(() => Text('V ${controller.latestVersion}', SizedBox(width: 6.w),
style: TextStyle( Text(
fontSize: 24, '已是最新版本',
fontWeight: FontWeight.normal, style: TextStyle(
color: isDarkMode ? Colors.blue[300] : Colors.blue))), fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Colors.green,
),
),
], ],
), ),
], );
); }
});
}
// 添加固件更新说明 // 固件更新说明
Widget _buildUpdateNotes(bool isDarkMode) => Widget _buildUpdateNotes(bool isDarkMode) {
Obx(() => controller.isUpdateAvailable.value return Obx(() => controller.isUpdateAvailable.value &&
? Card( controller.updatedescription.value.isNotEmpty
elevation: 2, ? Container(
shape: RoundedRectangleBorder( width: double.infinity,
borderRadius: BorderRadius.circular(16), margin: EdgeInsets.only(bottom: 8.h),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [
Colors.amber[900]!.withValues(alpha: 0.1),
Colors.orange[900]!.withValues(alpha: 0.05),
]
: [
Colors.amber[50]!,
Colors.orange[100]!.withValues(alpha: 0.3),
],
), ),
color: isDarkMode ? Colors.grey[900] : Colors.white, borderRadius: BorderRadius.circular(16.r),
child: Padding( border: Border.all(
padding: EdgeInsets.all(16), color: isDarkMode
child: Column( ? Colors.amber[700]!.withValues(alpha: 0.3)
crossAxisAlignment: CrossAxisAlignment.start, : Colors.amber[200]!.withValues(alpha: 0.5),
children: [ width: 1,
Row( ),
children: [ ),
Icon( child: Padding(
padding: EdgeInsets.all(20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.new_releases, Icons.new_releases,
color: isDarkMode ? Colors.amber[300] : Colors.amber, color: isDarkMode ? Colors.amber[300] : Colors.amber[700],
size: 20, size: 18.sp,
), ),
SizedBox(width: 8), ),
Text( SizedBox(width: 12.w),
'更新内容'.tr, Text(
style: TextStyle( '更新内容'.tr,
fontSize: 18, style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16.sp,
color: isDarkMode ? Colors.white : Colors.black, fontWeight: FontWeight.w600,
), color: isDarkMode ? Colors.amber[200] : Colors.amber[800],
), ),
], ),
],
),
SizedBox(height: 16.h),
Container(
width: double.infinity,
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.03)
: Colors.white.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(12.r),
), ),
Divider(), child: Text(
SizedBox(height: 8),
// 这里可以从controller中获取更新说明,如果有的话
Text(
controller.updatedescription.value, controller.updatedescription.value,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[800], height: 1.5,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
), ),
), ),
], ),
), ],
), ),
) ),
: SizedBox.shrink()); )
: const SizedBox.shrink());
}
Widget _buildLoadingIndicator(bool isDarkMode) => Column( // 加载指示器
Widget _buildLoadingIndicator(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(32.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.03)
: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: isDarkMode
? Colors.white.withValues(alpha: 0.08)
: Colors.grey.withValues(alpha: 0.2),
width: 1,
),
),
child: Column(
children: [ children: [
CircularProgressIndicator( // 自定义加载动画
valueColor: AlwaysStoppedAnimation( SizedBox(
isDarkMode ? Colors.blue[300] : Colors.blue), width: 50.w,
height: 50.w,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(
isDarkMode ? const Color(0xFF0066CC) : Colors.blue,
),
backgroundColor: isDarkMode
? Colors.white.withValues(alpha: 0.1)
: Colors.grey.withValues(alpha: 0.2),
),
),
SizedBox(height: 24.h),
Text(
'正在获取设备信息...',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
),
SizedBox(height: 8.h),
Text(
'请保持设备连接',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[500],
),
), ),
SizedBox(height: 20),
Text('正在获取设备信息...',
style:
TextStyle(color: isDarkMode ? Colors.grey[300] : Colors.grey))
], ],
); ),
);
}
Widget _buildErrorInfo(bool isDarkMode) => Container( // 错误信息显示
padding: EdgeInsets.all(16), Widget _buildErrorInfo(bool isDarkMode) {
decoration: BoxDecoration( return Container(
color: padding: EdgeInsets.all(24.w),
isDarkMode ? Colors.red[900]!.withOpacity(0.2) : Colors.red[50], decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), gradient: LinearGradient(
border: Border.all( begin: Alignment.topLeft,
color: isDarkMode ? Colors.red[700]! : Colors.red[200]!, end: Alignment.bottomRight,
width: 1, colors: isDarkMode
), ? [
Colors.red[900]!.withValues(alpha: 0.15),
Colors.red[800]!.withValues(alpha: 0.1),
]
: [
Colors.red[50]!,
Colors.red[100]!.withValues(alpha: 0.3),
],
), ),
child: Column( borderRadius: BorderRadius.circular(20.r),
children: [ border: Border.all(
Icon(Icons.error_outline, color: Colors.red, size: 40), color: isDarkMode
SizedBox(height: 10), ? Colors.red[700]!.withValues(alpha: 0.3)
Text(controller.errorMessage.value, : Colors.red[200]!.withValues(alpha: 0.5),
textAlign: TextAlign.center, width: 1,
),
),
child: Column(
children: [
// 错误图标
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.red.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(50.r),
),
child: Icon(
Icons.error_outline,
color: Colors.red,
size: 32.sp,
),
),
SizedBox(height: 16.h),
// 错误标题
Text(
'连接失败',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.red[300] : Colors.red[700],
),
),
SizedBox(height: 8.h),
// 错误信息
Text(
controller.errorMessage.value,
textAlign: TextAlign.center,
style: TextStyle(
color: isDarkMode ? Colors.red[200] : Colors.red[600],
fontSize: 14.sp,
height: 1.4,
),
),
SizedBox(height: 24.h),
// 重试按钮
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: Icon(Icons.refresh, size: 18.sp),
label: Text(
'重试连接'.tr,
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.red[300] : Colors.red, fontSize: 16.sp,
fontSize: 16)), fontWeight: FontWeight.w500,
SizedBox(height: 20), ),
ElevatedButton.icon( ),
icon: Icon(Icons.refresh),
label: Text('重试连接'.tr),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? Colors.blue[700] : Colors.blue, backgroundColor: isDarkMode ? const Color(0xFF0066CC) : Colors.blue,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
), ),
elevation: 0,
), ),
onPressed: controller.retryConnection, onPressed: controller.retryConnection,
) ),
], ),
), ],
); ),
);
}
// 更新按钮
Widget _buildUpdateButton(bool isDarkMode) { Widget _buildUpdateButton(bool isDarkMode) {
return controller.isUpgrade.value return Obx(() {
? _buildProgressIndicator(isDarkMode) if (controller.isUpgrade.value) {
: Container( return _buildProgressIndicator(isDarkMode);
width: double.infinity, }
padding: EdgeInsets.symmetric(vertical: 10),
child: Column( return SizedBox(
children: [ width: double.infinity,
ElevatedButton.icon( child: Column(
icon: Icon( children: [
controller.isUpdateAvailable.value // 主要更新按钮
? Icons.system_update Container(
: Icons.check_circle, width: double.infinity,
height: 56.h,
decoration: BoxDecoration(
gradient: controller.isUpdateAvailable.value
? LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: isDarkMode
? [const Color(0xFF0066CC), const Color(0xFF0052A3)]
: [Colors.blue, Colors.blue[700]!],
)
: null,
color: !controller.isUpdateAvailable.value
? (isDarkMode ? Colors.grey[800] : Colors.grey[300])
: null,
borderRadius: BorderRadius.circular(16.r),
boxShadow: controller.isUpdateAvailable.value
? [
BoxShadow(
color: Colors.blue.withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
]
: null,
),
child: ElevatedButton.icon(
icon: Icon(
controller.isUpdateAvailable.value
? Icons.system_update
: Icons.check_circle,
size: 20.sp,
),
label: Text(
controller.isUpdateAvailable.value ? '立即更新' : '已是最新版本',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
), ),
label: Text( ),
controller.isUpdateAvailable.value ? '立即更新' : '已是最新版本', style: ElevatedButton.styleFrom(
style: TextStyle(fontSize: 16), backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
), ),
style: ElevatedButton.styleFrom( ),
backgroundColor: controller.isUpdateAvailable.value onPressed: controller.isUpdateAvailable.value
? (isDarkMode ? Colors.blue[700] : Colors.blue) ? controller.startUpdate
: (isDarkMode ? Colors.grey[800] : Colors.grey), : null,
foregroundColor: Colors.white, ),
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), ),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30), // 更新提示信息
), if (controller.isUpdateAvailable.value) ...[
minimumSize: Size(240, 50), SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.orange.withValues(alpha: 0.1)
: Colors.orange[50],
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.orange.withValues(alpha: 0.3),
width: 1,
), ),
onPressed: controller.isUpdateAvailable.value
? controller.startUpdate
: null,
), ),
if (controller.isUpdateAvailable.value) ...[ child: Row(
SizedBox(height: 8), children: [
Text( Icon(
'取出双耳机,并保证电量均为30%以上的状态'.tr, Icons.info_outline,
style: TextStyle( color: Colors.orange,
fontSize: 14, size: 20.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
), ),
), SizedBox(width: 12.w),
], Expanded(
], child: Text(
), '取出双耳机,并保证电量均为30%以上的状态'.tr,
); style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.orange[200] : Colors.orange[800],
height: 1.3,
),
),
),
],
),
),
],
],
),
);
});
} }
// 进度指示器
Widget _buildProgressIndicator(bool isDarkMode) { Widget _buildProgressIndicator(bool isDarkMode) {
return Obx(() { return Obx(() {
final progress = controller.progress.value; final progress = controller.progress.value;
final type = controller.type.value; final type = controller.type.value;
// // 处理升级失败状态:弹出 snackbar 并显示错误提示 final statusMessage = controller.getStatusMessage(type);
// if (type == OtaUpgradeController.UPGRADE_FAILED ||
// type == OtaUpgradeController.CONNECTION_DISCONNECTION) {
// // 使用延迟确保在 build 完成后执行
// Future.delayed(Duration.zero, () {
// Get.snackbar('升级失败', '重新连接设备后重试');
// });
// return Container(
// width: double.infinity,
// padding: const EdgeInsets.symmetric(vertical: 10),
// child: Text(
// '升级失败,请重新连接设备',
// style: TextStyle(
// fontSize: 18,
// fontWeight: FontWeight.bold,
// color: Colors.red[700],
// ),
// ),
// );
// }
// 正常升级状态
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10), padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [
const Color(0xFF1A1A1A).withValues(alpha: 0.9),
const Color(0xFF2A2A2A).withValues(alpha: 0.9),
]
: [
const Color(0xFFF8FAFF),
const Color(0xFFE3F2FD),
],
),
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column( child: Column(
children: [ children: [
Text( // 状态图标和文字
controller.getStatusMessage(type), Row(
style: TextStyle( mainAxisAlignment: MainAxisAlignment.center,
fontSize: 18, children: [
fontWeight: FontWeight.bold, Container(
color: isDarkMode ? Colors.blue[300] : Colors.blue[700], padding: EdgeInsets.all(8.w),
), decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.system_update,
color: isDarkMode ? const Color(0xFF0066CC) : Colors.blue,
size: 20.sp,
),
),
SizedBox(width: 12.w),
Text(
statusMessage,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
),
),
],
), ),
const SizedBox(height: 16), SizedBox(height: 24.h),
LinearProgressIndicator(
value: progress / 100.0, // 转换为 0.0-1.0 // 进度条容器
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200], Container(
valueColor: AlwaysStoppedAnimation<Color>( height: 8.h,
isDarkMode ? Colors.blue[300]! : Colors.blue, decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.1)
: Colors.grey[200],
borderRadius: BorderRadius.circular(4.r),
), ),
), child: ClipRRect(
const SizedBox(height: 8), borderRadius: BorderRadius.circular(4.r),
Text( child: LinearProgressIndicator(
'${progress.toStringAsFixed(1)}%', value: progress / 100.0,
style: TextStyle( backgroundColor: Colors.transparent,
fontSize: 16, valueColor: AlwaysStoppedAnimation<Color>(
fontWeight: FontWeight.bold, isDarkMode ? const Color(0xFF0066CC) : Colors.blue,
color: isDarkMode ? Colors.blue[300] : Colors.blue[700], ),
),
), ),
), ),
SizedBox(height: 16.h),
// 进度百分比和详细信息
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${progress.toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
),
),
Text(
'请勿断开连接',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
],
),
], ],
), ),
); );

46
lib/modules/pairing/views/pairing_view.dart

@ -305,44 +305,44 @@ class PairingView extends GetView<PairingController> {
String _getSubtitleForState(PairingState state) { String _getSubtitleForState(PairingState state) {
switch (state) { switch (state) {
case PairingState.idle: case PairingState.idle:
return '准备开始配对'; return 'readyToPair'.tr; // 准备开始配对
case PairingState.checking: case PairingState.checking:
return '正在准备配对环境...'; return 'preparingPairingEnvironment'.tr; // 正在准备配对环境...
case PairingState.waitingClassic: case PairingState.waitingClassic:
return '请先通过系统蓝牙连接DeapSound设备'; return 'connectDeepSoundViaSystemBluetooth'.tr; // 请先通过系统蓝牙连接DeapSound设备
case PairingState.verifying: case PairingState.verifying:
return '正在验证设备...'; return 'verifyingDevice'.tr; // 正在验证设备...
case PairingState.binding: case PairingState.binding:
return '设备绑定确认...'; return 'deviceBindingConfirm'.tr; // 设备绑定确认...
case PairingState.associating: case PairingState.associating:
return '正在建立设备关联...'; return 'establishingDeviceAssociation'.tr; // 正在建立设备关联...
case PairingState.waitingBle: case PairingState.waitingBle:
return '请稍候,正在完成配对...'; return 'waitingCompletePairing'.tr; // 请稍候,正在完成配对...
case PairingState.completed: case PairingState.completed:
return '配对成功!即将跳转到主页...'; return 'pairingSuccessRedirecting'.tr; // 配对成功!即将跳转到主页...
case PairingState.error: case PairingState.error:
final errorType = controller.errorType; final errorType = controller.errorType;
switch (errorType) { switch (errorType) {
case ErrorType.bluetoothOff: case ErrorType.bluetoothOff:
return '请开启蓝牙后重试'; return 'bluetoothTurnedOff'.tr; // 蓝牙已关闭,请打开蓝牙
case ErrorType.noPermission: case ErrorType.noPermission:
return '需要蓝牙权限才能继续'; return 'lackPermissions'.tr; // 缺少蓝牙权限
case ErrorType.noClassicDevice: case ErrorType.noClassicDevice:
return '请先通过系统蓝牙连接DeapSound设备'; return 'connectDeepSoundViaSystemBluetooth'.tr; // 请先通过系统蓝牙连接DeapSound设备
case ErrorType.deviceNotMatch: case ErrorType.deviceNotMatch:
return '请确保连接的是DeapSound设备'; return 'ensureConnectedDeepSoundDevice'.tr; // 请确保连接的是DeapSound设备
case ErrorType.deviceBound: case ErrorType.deviceBound:
return '该设备已被其他账号绑定'; return 'deviceAlreadyBoundToOtherAccount'.tr; // 该设备已被其他账号绑定
case ErrorType.associationFailed: case ErrorType.associationFailed:
return '设备关联失败,请重试'; return 'deviceAssociationFailed'.tr; // 设备关联失败,请重试
case ErrorType.bleTimeout: case ErrorType.bleTimeout:
return '扫描超时,请确保设备在附近'; return 'scanTimeoutEnsureDeviceNearby'.tr; // 扫描超时,请确保设备在附近
case ErrorType.networkError: case ErrorType.networkError:
return '网络错误,请检查网络连接'; return 'networkError'.tr; // 网络错误
case ErrorType.authnFailed: case ErrorType.authnFailed:
return '授权失败,请检查授权码是否正确'; return 'authorizationFailedCheckCode'.tr; // 授权失败,请检查授权码是否正确
default: default:
return '配对失败,请重试'; return 'pairingFailedRetry'.tr; // 配对失败,请重试
} }
default: default:
return ''; return '';
@ -489,7 +489,7 @@ class PairingView extends GetView<PairingController> {
minimumSize: Size(double.infinity, 56.h), minimumSize: Size(double.infinity, 56.h),
), ),
child: Text( child: Text(
"绑定", "bind".tr, // 绑定
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -530,7 +530,7 @@ class PairingView extends GetView<PairingController> {
controller: controller controller: controller
.codeInputController, // 你可以在 Controller 中声明这个 .codeInputController, // 你可以在 Controller 中声明这个
decoration: InputDecoration( decoration: InputDecoration(
hintText: '请输入代码', hintText: 'enterCode'.tr, // 请输入代码
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
), ),
@ -552,7 +552,7 @@ class PairingView extends GetView<PairingController> {
elevation: 0, elevation: 0,
), ),
child: Text( child: Text(
'授权绑定', 'authorizeBind'.tr, // 授权绑定
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -596,7 +596,7 @@ class PairingView extends GetView<PairingController> {
minimumSize: Size(double.infinity, 48.h), minimumSize: Size(double.infinity, 48.h),
), ),
child: Text( child: Text(
'跳过绑定'.tr, // 稍后再说 'skipBinding'.tr, // 跳过绑定
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -615,7 +615,7 @@ class PairingView extends GetView<PairingController> {
minimumSize: Size(double.infinity, 48.h), minimumSize: Size(double.infinity, 48.h),
), ),
child: Text( child: Text(
'重新扫描'.tr, // 稍后再说 'rescan'.tr, // 重新扫描
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,

14
lib/modules/pairing/views/pairing_view_new.dart

@ -45,7 +45,7 @@ class PairingView extends GetView<PairingController> {
return Column( return Column(
children: [ children: [
Text( Text(
'设备配对', 'devicePairing'.tr, // 设备配对
style: TextStyle( style: TextStyle(
fontSize: 24.sp, fontSize: 24.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -54,7 +54,7 @@ class PairingView extends GetView<PairingController> {
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
Text( Text(
'连接您的 DeepSound 耳机', 'connectYourDeepSoundHeadset'.tr, // 连接您的 DeepSound 耳机
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -208,7 +208,7 @@ class PairingView extends GetView<PairingController> {
// 主要操作按钮 // 主要操作按钮
if (showConnectButton) if (showConnectButton)
_buildPrimaryButton( _buildPrimaryButton(
'连接设备', 'connectDevice'.tr, // 连接设备
controller.onConnectTap, controller.onConnectTap,
isDarkMode, isDarkMode,
), ),
@ -218,7 +218,7 @@ class PairingView extends GetView<PairingController> {
TextButton( TextButton(
onPressed: controller.confirmBindDevice, onPressed: controller.confirmBindDevice,
child: Text( child: Text(
'绑定', 'bind'.tr, // 绑定
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -228,7 +228,7 @@ class PairingView extends GetView<PairingController> {
if (showRetryButton) if (showRetryButton)
_buildPrimaryButton( _buildPrimaryButton(
'重试', 'retry'.tr, // 重试
controller.onRetryTap, controller.onRetryTap,
isDarkMode, isDarkMode,
), ),
@ -250,7 +250,7 @@ class PairingView extends GetView<PairingController> {
elevation: 0, elevation: 0,
), ),
child: Text( child: Text(
'自动输入代码', 'autoInputCode'.tr, // 自动输入代码
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -265,7 +265,7 @@ class PairingView extends GetView<PairingController> {
TextButton( TextButton(
onPressed: controller.onSkipTap, onPressed: controller.onSkipTap,
child: Text( child: Text(
'跳过', 'skip'.tr, // 跳过
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], color: isDarkMode ? Colors.grey[400] : Colors.grey[600],

2
lib/modules/settings/views/settings_view.dart

@ -841,7 +841,7 @@ class SettingsView extends GetView<SettingsController> {
//------------测试使用标记 start---------------// //------------测试使用标记 start---------------//
SizedBox(height: 4.h), SizedBox(height: 4.h),
Text( Text(
'${Platform.operatingSystem}-test-0626', '${Platform.operatingSystem}-test-0707',
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: const Color.fromARGB(255, 143, 94, 94), color: const Color.fromARGB(255, 143, 94, 94),

223
lib/modules/translation/controllers/translation_controller.dart

@ -10,6 +10,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/music_manager.dart';
import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/tts_service.dart'; import '../../../data/services/tts_service.dart';
import '../../../data/services/language_manager.dart'; import '../../../data/services/language_manager.dart';
@ -35,6 +36,8 @@ class TranslationController extends GetxController {
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
// 蓝牙服务 // 蓝牙服务
final bleManager = Get.find<BleManager>(); final bleManager = Get.find<BleManager>();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
// 音频输入源 // 音频输入源
bool _audioSourceType = false; bool _audioSourceType = false;
//是否开启播放 //是否开启播放
@ -45,14 +48,20 @@ class TranslationController extends GetxController {
Timer? _recordTimer; Timer? _recordTimer;
//秒 //秒
int _seconds = 0; int _seconds = 0;
// 存储相关 // 存储相关配置
static const String _historyKey = 'translation_history'; static const String _historyKey = 'translation_history';
static const String _sourceLanguageKey = 'translation_source_language'; static const String _sourceLanguageKey = 'translation_source_language';
static const String _targetLanguageKey = 'translation_target_language'; static const String _targetLanguageKey = 'translation_target_language';
static const int _maxHistoryItems = 10;
// 【重要改进】区分存储限制和显示限制
// 存储限制:大幅增加到1000条,充分利用本地存储能力,几乎无限制存储
static const int _maxStorageItems = 1000;
// 主页面显示限制:只显示最近20条,保证界面性能和用户体验
static const int _maxDisplayItems = 20;
RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2 RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2
RxInt lastActiveSpeaker = 0.obs; // 上一次状态 RxInt lastActiveSpeaker = 0.obs; // 上一次状态
bool isContinuePlaying = false; // 是否继续播放
// 状态变量 // 状态变量
final isRecognizing = false.obs; final isRecognizing = false.obs;
final isTranslating = false.obs; final isTranslating = false.obs;
@ -172,7 +181,7 @@ class TranslationController extends GetxController {
// 初始化服务 // 初始化服务
_initServices(); _initServices();
suspendOtherServices();
// 使用微任务在布局完成后自动滚动到底部 // 使用微任务在布局完成后自动滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom(); _scrollToBottom();
@ -247,6 +256,32 @@ class TranslationController extends GetxController {
} }
} }
// 暂停其他服务
Future<void> suspendOtherServices() async {
try {
var status = await _musiceManager.getStatus();
var isPlaying = status['isPlaying'] ?? false;
if (isPlaying == true) {
isContinuePlaying = true;
await _musiceManager.pause(); // 暂停音乐
} else {
isContinuePlaying = false;
}
} catch (e) {
Logger.error('初始化服务失败: ${e.toString()}');
}
}
// 恢复其他服务
Future<void> restoreOtherServices() async {
try {
if (isContinuePlaying) await _musiceManager.resume(); // 暂停音乐
} catch (e) {
Logger.error('初始化服务失败: ${e.toString()}');
}
}
// 初始化服务 // 初始化服务
Future<void> _initServices() async { Future<void> _initServices() async {
try { try {
@ -285,6 +320,7 @@ class TranslationController extends GetxController {
_voiceInteractionSubscription?.cancel(); _voiceInteractionSubscription?.cancel();
_translationDebounceTimer?.cancel(); _translationDebounceTimer?.cancel();
scrollController.dispose(); scrollController.dispose();
restoreOtherServices();
super.onClose(); super.onClose();
} }
@ -559,8 +595,8 @@ class TranslationController extends GetxController {
String? translationResult; String? translationResult;
try { try {
var detectedSourceLanguageCode; String detectedSourceLanguageCode;
var detectedTargetLanguageCode; String detectedTargetLanguageCode;
if ((currentMode.value == 'faceToFace' && activeSpeaker.value == 1) || if ((currentMode.value == 'faceToFace' && activeSpeaker.value == 1) ||
(currentMode.value == 'faceToFace' && (currentMode.value == 'faceToFace' &&
activeSpeaker.value == 0 && activeSpeaker.value == 0 &&
@ -608,7 +644,12 @@ class TranslationController extends GetxController {
translationHistory[index].targetLanguageCode = translationHistory[index].targetLanguageCode =
detectedTargetLanguageCode; detectedTargetLanguageCode;
translationHistory.refresh(); translationHistory.refresh();
saveTranslationHistory();
// 【改进】只有在最终结果时才保存,避免中间态时的频繁保存
if (isFinal) {
saveTranslationHistory();
}
// 更新后滚动到底部 // 更新后滚动到底部
_scrollToBottom(); _scrollToBottom();
// 播放TTS - 只在最终结果时播放 // 播放TTS - 只在最终结果时播放
@ -629,8 +670,7 @@ class TranslationController extends GetxController {
} }
} }
} else { } else {
Logger.info( Logger.info('未找到匹配的历史项 $sourceText ${timestamp.toIso8601String()}');
'未找到匹配的历史项 ${sourceText} ${timestamp.toIso8601String() ?? "无时间戳"}');
} }
} }
} catch (e) { } catch (e) {
@ -836,10 +876,13 @@ class TranslationController extends GetxController {
final List<TranslationItem> allHistory = final List<TranslationItem> allHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList(); historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 只保留最新的10条记录 // 【注意】存储中的记录都是完成态记录(isIntermediate = false)
if (allHistory.length > _maxHistoryItems) { // 因为保存时已经过滤掉了中间态记录,确保数据质量
// 主页面只显示最近的20条完成态记录,保证界面性能
if (allHistory.length > _maxDisplayItems) {
translationHistory.value = translationHistory.value =
allHistory.sublist(allHistory.length - _maxHistoryItems); allHistory.sublist(allHistory.length - _maxDisplayItems);
} else { } else {
translationHistory.value = allHistory; translationHistory.value = allHistory;
} }
@ -855,38 +898,170 @@ class TranslationController extends GetxController {
} }
} }
// 保存翻译历史 // 保存翻译历史 - 回归原始简单实现
// 【修复】完全回归原始保存逻辑,只保存当前内存中的完成态记录
void saveTranslationHistory() { void saveTranslationHistory() {
if (translationHistory.length > _maxHistoryItems) {
translationHistory.removeRange(
0, translationHistory.length - _maxHistoryItems);
}
try { try {
// 只保存当前内存中的完成态记录,过滤掉中间态记录
final List<TranslationItem> finalRecords =
translationHistory.where((item) => !item.isIntermediate).toList();
// 应用存储限制,保留最新的1000条完成态记录
List<TranslationItem> historyToSave = finalRecords;
if (finalRecords.length > _maxStorageItems) {
historyToSave =
finalRecords.sublist(finalRecords.length - _maxStorageItems);
}
// 保存到本地存储
final List<Map<String, dynamic>> historyJson = final List<Map<String, dynamic>> historyJson =
translationHistory.map((item) => item.toJson()).toList(); historyToSave.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson)); _storage.write(_historyKey, jsonEncode(historyJson));
Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录');
} catch (e) { } catch (e) {
Logger.error('保存翻译历史失败: ${e.toString()}'); Logger.error('保存翻译历史失败: ${e.toString()}');
} }
} }
// 获取所有完成态历史记录(供历史页面使用)
// 【修复】简化实现,合并存储和内存中的完成态记录
List<TranslationItem> getAllHistory() {
try {
final List<TranslationItem> result = <TranslationItem>[];
final Set<String> existingIds = <String>{};
// 添加存储的历史记录(存储中的记录都是完成态)
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
for (final item in storedHistory) {
final id =
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}';
if (!existingIds.contains(id)) {
existingIds.add(id);
result.add(item);
}
}
}
// 添加当前内存中的完成态记录(排除中间态记录)
for (final item in translationHistory) {
if (!item.isIntermediate) {
final id =
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}';
if (!existingIds.contains(id)) {
existingIds.add(id);
result.add(item);
}
}
}
// 按时间排序
result.sort((a, b) => a.timestamp.compareTo(b.timestamp));
return result;
} catch (e) {
Logger.error('获取完整历史记录失败: ${e.toString()}');
// 发生错误时,返回当前内存中的完成态记录
return translationHistory.where((item) => !item.isIntermediate).toList();
}
}
// 清除翻译历史 // 清除翻译历史
void clearTranslationHistory() { void clearTranslationHistory() {
translationHistory.clear(); translationHistory.clear();
_storage.remove(_historyKey); _storage.remove(_historyKey);
} }
// 删除指定会话的所有翻译记录
void deleteSessionHistory(String sessionId) {
try {
// 1. 从内存中删除该会话的所有记录
final originalCount = translationHistory.length;
translationHistory.removeWhere((item) => item.sessionId == sessionId);
final removedFromMemory = originalCount - translationHistory.length;
// 2. 从本地存储中删除该会话的记录
final historyJson = _storage.read(_historyKey);
int removedFromStorage = 0;
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
final originalStoredCount = storedHistory.length;
storedHistory.removeWhere((item) => item.sessionId == sessionId);
removedFromStorage = originalStoredCount - storedHistory.length;
// 重新保存更新后的存储数据
final List<Map<String, dynamic>> updatedHistoryJson =
storedHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(updatedHistoryJson));
}
// 3. 刷新UI显示
translationHistory.refresh();
Logger.info(
'删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条');
} catch (e) {
Logger.error('删除会话历史失败: ${e.toString()}');
rethrow; // 重新抛出异常,让调用方处理
}
}
// 删除指定的单条翻译记录
void deleteTranslationItem(String sessionId, DateTime timestamp) {
try {
// 1. 从内存中删除指定的翻译记录
final originalCount = translationHistory.length;
translationHistory.removeWhere((item) =>
item.sessionId == sessionId &&
item.timestamp.millisecondsSinceEpoch ==
timestamp.millisecondsSinceEpoch);
final removedFromMemory = originalCount - translationHistory.length;
// 2. 从本地存储中删除指定的翻译记录
final historyJson = _storage.read(_historyKey);
int removedFromStorage = 0;
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
final originalStoredCount = storedHistory.length;
storedHistory.removeWhere((item) =>
item.sessionId == sessionId &&
item.timestamp.millisecondsSinceEpoch ==
timestamp.millisecondsSinceEpoch);
removedFromStorage = originalStoredCount - storedHistory.length;
// 重新保存更新后的存储数据
final List<Map<String, dynamic>> updatedHistoryJson =
storedHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(updatedHistoryJson));
}
// 3. 刷新UI显示
translationHistory.refresh();
Logger.info(
'删除单条翻译记录成功: sessionId=$sessionId, timestamp=${timestamp.toIso8601String()}, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条');
} catch (e) {
Logger.error('删除单条翻译记录失败: ${e.toString()}');
rethrow; // 重新抛出异常,让调用方处理
}
}
// 滚动到底部 (公共方法,用于向后兼容) // 滚动到底部 (公共方法,用于向后兼容)
void scrollToBottom({bool animate = true}) { void scrollToBottom({bool animate = true}) {
_scrollToBottom(); _scrollToBottom();
} }
/// 处理原生语音交互开始事件
void _handleVoiceInteractionStarted() {
// 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS
stopAll();
}
} }
// 翻译项模型 // 翻译项模型

64
lib/modules/translation/controllers/translation_detail_controller.dart

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../data/services/volcano_ai_service.dart'; import '../../../data/services/volcano_ai_service.dart';
import '../controllers/translation_controller.dart'; import '../controllers/translation_controller.dart';
@ -50,7 +51,7 @@ class TranslationDetailController extends GetxController {
// 异步初始化方法 // 异步初始化方法
Future<void> _asyncInitialize() async { Future<void> _asyncInitialize() async {
isLoading.value = true; isLoading.value = true;
try { try {
// 使用 Future.microtask 将操作放到后台线程 // 使用 Future.microtask 将操作放到后台线程
await Future.microtask(() { await Future.microtask(() {
@ -116,13 +117,13 @@ class TranslationDetailController extends GetxController {
Future<String> extractSummary(String text) async { Future<String> extractSummary(String text) async {
return _extractSummaryInternal(text, _defaultTimeoutDuration); return _extractSummaryInternal(text, _defaultTimeoutDuration);
} }
// 内部摘要提取方法,避免代码重复 // 内部摘要提取方法,避免代码重复
Future<String> _extractSummaryInternal(String text, int timeoutDuration) async { Future<String> _extractSummaryInternal(String text, int timeoutDuration) async {
if (text.isEmpty) { if (text.isEmpty) {
return '无法生成摘要:文本为空'; return '无法生成摘要:文本为空';
} }
isGeneratingSummary.value = true; isGeneratingSummary.value = true;
try { try {
@ -155,7 +156,7 @@ class TranslationDetailController extends GetxController {
messages: messages, messages: messages,
systemPrompt: systemPrompt, systemPrompt: systemPrompt,
).timeout(Duration(seconds: timeoutDuration), onTimeout: () { ).timeout(Duration(seconds: timeoutDuration), onTimeout: () {
throw TimeoutException('请求超时,请稍后重试'); throw TimeoutException('请求超时', Duration(seconds: timeoutDuration));
}); });
} on TimeoutException catch (_) { } on TimeoutException catch (_) {
final timeoutMessage = timeoutDuration == _defaultTimeoutDuration final timeoutMessage = timeoutDuration == _defaultTimeoutDuration
@ -182,10 +183,63 @@ class TranslationDetailController extends GetxController {
} }
} }
// 删除单条翻译记录
Future<void> deleteTranslationItem(TranslationItem item) async {
try {
// 调用主控制器的删除方法
historyController.translationController.deleteTranslationItem(
item.sessionId,
item.timestamp
);
// 从当前会话项目列表中移除该项目
sessionItems.removeWhere((sessionItem) =>
sessionItem.sessionId == item.sessionId &&
sessionItem.timestamp.millisecondsSinceEpoch == item.timestamp.millisecondsSinceEpoch);
// 重新加载历史控制器的数据
await historyController.loadHistoryData();
// 检查是否删除了会话中的最后一条记录
if (sessionItems.isEmpty) {
// 如果会话为空,返回上级页面
Get.back();
Get.snackbar(
'删除成功',
'已删除该翻译记录,会话已清空',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(seconds: 2),
);
} else {
// 显示删除成功提示
Get.snackbar(
'删除成功',
'已删除该翻译记录',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(seconds: 2),
);
}
} catch (e) {
// 显示删除失败提示
Get.snackbar(
'删除失败',
'删除翻译记录时发生错误: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
}
@override @override
void onClose() { void onClose() {
// 确保资源被正确释放 // 确保资源被正确释放
historyController.translationController.stopAll(); historyController.translationController.stopAll();
super.onClose(); super.onClose();
} }
} }

50
lib/modules/translation/controllers/translation_history_controller.dart

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../controllers/translation_controller.dart'; import '../controllers/translation_controller.dart';
@ -44,9 +45,12 @@ class TranslationHistoryController extends GetxController {
// 按日期对历史记录进行分组 // 按日期对历史记录进行分组
void _groupHistoryByDate() { void _groupHistoryByDate() {
final Map<String, List<TranslationItem>> newGroupedHistory = {}; final Map<String, List<TranslationItem>> newGroupedHistory = {};
// 获取完整的历史记录,而不仅仅是当前显示的记录
final List<TranslationItem> allHistory = translationController.getAllHistory();
// 筛选出每个会话的第一条记录 // 筛选出每个会话的第一条记录
final List<TranslationItem> firstItemsOfSessions = translationController.translationHistory final List<TranslationItem> firstItemsOfSessions = allHistory
.where((item) => item.isFirstInSession) .where((item) => item.isFirstInSession)
.toList(); .toList();
@ -98,12 +102,15 @@ class TranslationHistoryController extends GetxController {
// 按会话ID对历史记录进行分组 // 按会话ID对历史记录进行分组
void _groupHistoryBySession() { void _groupHistoryBySession() {
final Map<String, List<TranslationItem>> newSessionMap = {}; final Map<String, List<TranslationItem>> newSessionMap = {};
for (var item in translationController.translationHistory) { // 获取完整的历史记录进行会话分组
final List<TranslationItem> allHistory = translationController.getAllHistory();
for (var item in allHistory) {
if (!newSessionMap.containsKey(item.sessionId)) { if (!newSessionMap.containsKey(item.sessionId)) {
newSessionMap[item.sessionId] = []; newSessionMap[item.sessionId] = [];
} }
newSessionMap[item.sessionId]!.add(item); newSessionMap[item.sessionId]!.add(item);
} }
@ -133,4 +140,35 @@ class TranslationHistoryController extends GetxController {
// 检查是否有历史记录 // 检查是否有历史记录
bool get hasHistory => translationController.translationHistory.isNotEmpty; bool get hasHistory => translationController.translationHistory.isNotEmpty;
}
// 删除指定会话的历史记录
Future<void> deleteSessionHistory(String sessionId) async {
try {
// 调用主控制器的删除方法
translationController.deleteSessionHistory(sessionId);
// 重新加载和分组历史数据
await loadHistoryData();
// 显示删除成功提示
Get.snackbar(
'删除成功',
'已删除该会话的所有翻译记录',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(seconds: 2),
);
} catch (e) {
// 显示删除失败提示
Get.snackbar(
'删除失败',
'删除会话记录时发生错误: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
}
}

313
lib/modules/translation/views/translation_detail_view.dart

@ -72,14 +72,14 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
), ),
onPressed: () => Get.back(), onPressed: () => Get.back(),
), ),
title: Text( // title: Text(
'Detail', // 'Detail',
style: TextStyle( // style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, // color: isDarkMode ? Colors.white : Colors.black,
fontSize: 24.sp, // fontSize: 24.sp,
fontWeight: FontWeight.bold, // fontWeight: FontWeight.bold,
), // ),
), // ),
centerTitle: true, centerTitle: true,
actions: [ actions: [
IconButton( IconButton(
@ -102,21 +102,22 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
children: [ children: [
Expanded( Expanded(
child: Obx(() => _buildTabButton( child: Obx(() => _buildTabButton(
title: '原始记录', title: 'originalRecord'.tr, // 原始记录
isSelected: controller.selectedTabIndex.value == 0, isSelected: controller.selectedTabIndex.value == 0,
onTap: () => controller.switchTab(0), onTap: () => controller.switchTab(0),
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
)), )),
), ),
SizedBox(width: 12.w), //屏蔽“记录摘要”,需要是开启
Expanded( // SizedBox(width: 12.w),
child: Obx(() => _buildTabButton( // Expanded(
title: '记录摘要', // child: Obx(() => _buildTabButton(
isSelected: controller.selectedTabIndex.value == 1, // title: 'recordSummary'.tr, // 记录摘要
onTap: () => controller.switchTab(1), // isSelected: controller.selectedTabIndex.value == 1,
isDarkMode: isDarkMode, // onTap: () => controller.switchTab(1),
)), // isDarkMode: isDarkMode,
), // )),
// ),
], ],
), ),
); );
@ -210,85 +211,105 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
// 构建会话详情中的翻译项 // 构建会话详情中的翻译项
Widget _buildSessionItemWidget(TranslationItem item, bool isDarkMode) { Widget _buildSessionItemWidget(TranslationItem item, bool isDarkMode) {
final context = Get.context!;
return Container( return Container(
width: double.infinity, margin: const EdgeInsets.only(bottom: 8),
margin: EdgeInsets.only(bottom: 16.h), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white, color: item.isIntermediate
borderRadius: BorderRadius.circular(16.r), ? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色
boxShadow: [ : (item.sourceLanguageCode == controller.currentItem.sourceLanguageCode
BoxShadow( ? Theme.of(context)
color: Colors.black.withAlpha(isDarkMode ? 30 : 8), .colorScheme
blurRadius: 8, .primary
offset: Offset(0, 2), .withOpacity(0.08) // 源语言匹配时使用主题色底色
), : Theme.of(context)
], .colorScheme
.secondary
.withOpacity(0.08)), // 否则使用次要色底色
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: item.isIntermediate
? Colors.grey.withOpacity(0.2)
: Colors.transparent,
width: 1,
),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 源文本部分 // 源文本
Padding( Text(
padding: EdgeInsets.fromLTRB(20.r, 20.r, 20.r, 10.r), item.sourceText,
child: Text( style: TextStyle(
item.sourceText, fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
),
if (item.translatedText.isNotEmpty) ...[
const SizedBox(height: 8),
// 翻译文本
Text(
item.translatedText,
style: TextStyle( style: TextStyle(
fontSize: 15.sp, fontSize: 16,
color: isDarkMode ? Colors.white : Colors.black, color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
), ],
// 翻译文本部分 // 语言信息和操作按钮
Padding( const SizedBox(height: 4),
padding: EdgeInsets.fromLTRB(20.r, 10.r, 20.r, 16.r), Row(
child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end, children: [
children: [ Text(
Expanded( '${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}',
child: Text( style: TextStyle(
item.translatedText, fontSize: 12,
style: TextStyle( color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
fontWeight: FontWeight.normal,
),
),
), ),
SizedBox(width: 12.w), ),
_buildPlayButton(() => controller.playTranslationItem(item), isDarkMode), Row(
], mainAxisSize: MainAxisSize.min,
), children: [
// 删除按钮
if (!item.isIntermediate)
InkWell(
onTap: () => _showDeleteItemConfirmDialog(item),
borderRadius: BorderRadius.circular(6.r),
splashColor: Colors.red.withValues(alpha: 0.1),
highlightColor: Colors.red.withValues(alpha: 0.05),
child: Container(
padding: EdgeInsets.all(6.w),
child: Icon(
Icons.delete_outline,
size: 16.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
),
),
),
// 播放按钮
if (!item.isIntermediate) ...[
SizedBox(width: 8.w),
GestureDetector(
onTap: () => controller.playTranslationItem(item),
child: Icon(
Icons.volume_up,
size: 20.sp,
color: Theme.of(context).colorScheme.primary,
),
),
],
],
),
],
), ),
], ],
), ),
); );
} }
// 构建播放按钮
Widget _buildPlayButton(VoidCallback onTap, bool isDarkMode) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20.r),
splashColor: isDarkMode ? AppColors.primary.withOpacity(0.2) : Colors.blue.withOpacity(0.2),
child: Container(
width: 28.w,
height: 28.h,
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary.withAlpha(10) : Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.volume_up,
color: Colors.white,
size: 16.sp,
),
),
),
);
}
// 构建总结视图 // 构建总结视图
Widget _buildSummaryWidget(bool isDarkMode) { Widget _buildSummaryWidget(bool isDarkMode) {
@ -340,7 +361,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
padding: EdgeInsets.all(24.r), padding: EdgeInsets.all(24.r),
child: Center( child: Center(
child: Text( child: Text(
'点击下方按钮生成会话摘要', 'clickToGenerateSummary'.tr, // 点击下方按钮生成会话摘要
style: TextStyle( style: TextStyle(
fontSize: 15.sp, fontSize: 15.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -363,7 +384,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'摘要内容', 'summaryContent'.tr, // 摘要内容
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -438,8 +459,8 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
void _copySummaryToClipboard() { void _copySummaryToClipboard() {
Clipboard.setData(ClipboardData(text: controller.summaryResult.value)); Clipboard.setData(ClipboardData(text: controller.summaryResult.value));
Get.snackbar( Get.snackbar(
'已复制', 'copied'.tr, // 已复制
'摘要已复制到剪贴板', 'summaryCopiledToClipboard'.tr, // 摘要已复制到剪贴板
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 2), duration: Duration(seconds: 2),
); );
@ -469,7 +490,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
child: controller.isGeneratingSummary.value child: controller.isGeneratingSummary.value
? _buildGeneratingIndicator(isDarkMode) ? _buildGeneratingIndicator(isDarkMode)
: Text( : Text(
controller.summaryResult.value.isEmpty ? '提取摘要' : '已生成摘要', controller.summaryResult.value.isEmpty ? 'extractSummary'.tr : 'summaryGenerated'.tr, // 提取摘要 : 已生成摘要
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -494,7 +515,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
), ),
SizedBox(width: 12.w), SizedBox(width: 12.w),
Text( Text(
'生成中...', 'generating'.tr, // 生成中...
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -524,7 +545,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
), ),
), ),
child: Text( child: Text(
'重新生成', 'regenerate'.tr, // 重新生成
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -549,14 +570,14 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
title: Text( title: Text(
'请求超时', 'requestTimeout'.tr, // 请求超时
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 18.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
content: Text( content: Text(
'生成摘要请求超时,可能是网络问题或服务器繁忙。', 'summaryTimeoutMessage'.tr, // 生成摘要请求超时,可能是网络问题或服务器繁忙。
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
), ),
@ -565,7 +586,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
TextButton( TextButton(
onPressed: () => Get.back(), onPressed: () => Get.back(),
child: Text( child: Text(
'取消', 'cancel'.tr, // 取消
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
color: Colors.grey[700], color: Colors.grey[700],
@ -579,7 +600,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
controller.retryExtractSummaryWithExtendedTimeout(text); controller.retryExtractSummaryWithExtendedTimeout(text);
}, },
child: Text( child: Text(
'重试', 'retry'.tr, // 重试
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
color: Colors.blue, color: Colors.blue,
@ -595,23 +616,119 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
); );
} }
// 显示删除单条记录确认对话框
void _showDeleteItemConfirmDialog(TranslationItem item) {
final isDarkMode = Get.isDarkMode;
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] : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.sourceText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
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[400] : Colors.black54,
),
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();
// 删除指定的翻译记录
controller.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 _shareContent() { void _shareContent() {
String shareText = ''; String shareText = '';
if (controller.selectedTabIndex.value == 0) { if (controller.selectedTabIndex.value == 0) {
// 分享原始记录 // 分享原始记录
final String sourceText = _getCombinedSourceText(); final String sourceText = _getCombinedSourceText();
final String translatedText = _getCombinedTranslatedText(); final String translatedText = _getCombinedTranslatedText();
shareText = '原文:\n$sourceText\n\n译文:\n$translatedText'; shareText = '${'originalText'.tr}:\n$sourceText\n\n${'translatedText'.tr}:\n$translatedText'; // 原文:译文:
} else { } else {
// 分享摘要 // 分享摘要
if (controller.summaryResult.value.isNotEmpty) { if (controller.summaryResult.value.isNotEmpty) {
shareText = '会话摘要:\n${controller.summaryResult.value}'; shareText = '${'sessionSummary'.tr}:\n${controller.summaryResult.value}'; // 会话摘要:
} else { } else {
Get.snackbar( Get.snackbar(
'无法分享', 'cannotShare'.tr, // 无法分享
'请先生成摘要', 'generateSummaryFirst'.tr, // 请先生成摘要
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 2), duration: Duration(seconds: 2),
); );
@ -622,7 +739,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
// 调用系统分享 // 调用系统分享
Share.share( Share.share(
shareText, shareText,
subject: '翻译会话记录 - ${DateFormat('yyyy-MM-dd HH:mm').format(controller.currentItem.timestamp)}', subject: '${'translationSessionRecord'.tr} - ${DateFormat('yyyy-MM-dd HH:mm').format(controller.currentItem.timestamp)}', // 翻译会话记录
); );
} }
} }

134
lib/modules/translation/views/translation_history_view.dart

@ -1,11 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../controllers/translation_controller.dart'; import '../controllers/translation_controller.dart';
import '../controllers/translation_history_controller.dart'; import '../controllers/translation_history_controller.dart';
import '../bindings/translation_detail_binding.dart'; import '../bindings/translation_detail_binding.dart';
import 'translation_detail_view.dart'; import 'translation_detail_view.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../data/services/language_manager.dart';
class TranslationHistoryView extends GetView<TranslationHistoryController> { class TranslationHistoryView extends GetView<TranslationHistoryController> {
const TranslationHistoryView({Key? key}) : super(key: key); const TranslationHistoryView({Key? key}) : super(key: key);
@ -116,7 +118,7 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
@ -144,9 +146,52 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
SizedBox(height: 8.h),
// 时间和语种信息行
Row(
children: [
// 时间信息
Text(
_formatSessionTime(item.timestamp),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
),
),
SizedBox(width: 12.w),
// 语种信息
Expanded(
child: Text(
_formatLanguagePair(item),
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
),
],
),
], ],
), ),
), ),
SizedBox(width: 12.w),
// 删除会话按钮
InkWell(
onTap: () => _showDeleteSessionConfirmDialog(item.sessionId),
borderRadius: BorderRadius.circular(6.r),
splashColor: Colors.red.withOpacity(0.1),
highlightColor: Colors.red.withOpacity(0.05),
child: Container(
padding: EdgeInsets.all(8.w),
child: Icon(
Icons.delete_outline,
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
size: 18.sp,
),
),
),
SizedBox(width: 8.w),
Icon( Icon(
Icons.chevron_right, Icons.chevron_right,
color: isDarkMode ? Colors.grey[600] : Colors.grey[350], color: isDarkMode ? Colors.grey[600] : Colors.grey[350],
@ -161,7 +206,7 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
void _showTranslationDetail(TranslationItem item) { void _showTranslationDetail(TranslationItem item) {
// 获取该会话的所有翻译项 // 获取该会话的所有翻译项
final List<TranslationItem> sessionItems = controller.getSessionItems(item.sessionId); final List<TranslationItem> sessionItems = controller.getSessionItems(item.sessionId);
Get.to( Get.to(
() => TranslationDetailView(), () => TranslationDetailView(),
arguments: { arguments: {
@ -173,9 +218,92 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
); );
} }
// 格式化会话时间
String _formatSessionTime(DateTime timestamp) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final itemDate = DateTime(timestamp.year, timestamp.month, timestamp.day);
if (itemDate == today) {
// 今天:只显示时间 HH:mm
return DateFormat('HH:mm').format(timestamp);
} else {
// 其他日期:显示月日和时间 MM-dd HH:mm
return DateFormat('MM-dd HH:mm').format(timestamp);
}
}
// 格式化语种对信息
String _formatLanguagePair(TranslationItem item) {
final languageManager = Get.find<LanguageManager>();
// 获取源语言和目标语言的中文名称
final sourceName = languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ?? 'unknownLanguage'.tr; // 未知语言
final targetName = languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ?? 'unknownLanguage'.tr; // 未知语言
// 使用双向箭头符号表示翻译方向
return '$sourceName ⇄ $targetName';
}
// 显示删除会话确认对话框
void _showDeleteSessionConfirmDialog(String sessionId) {
final isDarkMode = Get.isDarkMode;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'deleteSessionRecord'.tr, // 删除会话记录
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Text(
'deleteSessionConfirm'.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: () {
Get.back();
// 删除指定会话的历史记录
controller.deleteSessionHistory(sessionId);
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
void _showClearHistoryConfirmDialog() { void _showClearHistoryConfirmDialog() {
final isDarkMode = Get.isDarkMode; final isDarkMode = Get.isDarkMode;
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,

28
lib/modules/translation/views/translation_list_view.dart

@ -37,7 +37,7 @@ class TranslationListView extends GetView<TranslationListController> {
onPressed: () => Get.back(), onPressed: () => Get.back(),
), ),
title: Text( title: Text(
"翻译录音文件", // "translateAudioFiles".tr, // 翻译录音文件
style: TextStyle( style: TextStyle(
fontSize: 17.sp, fontSize: 17.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -52,7 +52,7 @@ class TranslationListView extends GetView<TranslationListController> {
return Obx(() { return Obx(() {
final fileList = controller.files; // 不要 toList() final fileList = controller.files; // 不要 toList()
if (fileList.isEmpty) { if (fileList.isEmpty) {
return Center(child: Text('暂无音频文件')); return Center(child: Text('noAudioFiles'.tr)); // 暂无音频文件
} }
return ListView.builder( return ListView.builder(
@ -75,17 +75,17 @@ class TranslationListView extends GetView<TranslationListController> {
return FutureBuilder<Duration>( return FutureBuilder<Duration>(
future: controller.getAudioDuration(file.path), future: controller.getAudioDuration(file.path),
builder: (context, snapshot) { builder: (context, snapshot) {
String durationText = "计算中..."; String durationText = "calculating".tr; // 计算中...
if (snapshot.hasData) { if (snapshot.hasData) {
final duration = snapshot.data!; final duration = snapshot.data!;
if (duration.inSeconds > 0) { if (duration.inSeconds > 0) {
durationText = durationText =
"${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}"; "${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}";
} else { } else {
durationText = "未知"; durationText = "unknown".tr; // 未知
} }
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
durationText = "错误"; durationText = "error".tr; // 错误
} }
return GestureDetector( return GestureDetector(
onLongPressStart: (details) { onLongPressStart: (details) {
@ -216,8 +216,8 @@ class TranslationListView extends GetView<TranslationListController> {
// 播放状态文本 // 播放状态文本
Text( Text(
isCurrentFile isCurrentFile
? (isPlaying ? '播放中...' : '已暂停') ? (isPlaying ? 'playing'.tr : 'paused'.tr) // 播放中... : 已暂停
: '点击播放', : 'clickToPlay'.tr, // 点击播放
style: TextStyle( style: TextStyle(
color: Colors.blue, color: Colors.blue,
fontSize: 10.sp, fontSize: 10.sp,
@ -269,7 +269,7 @@ class TranslationListView extends GetView<TranslationListController> {
size: 20.w, size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
), ),
'重命名', 'rename'.tr, // 重命名
isDarkMode ? Colors.white : Colors.black87, isDarkMode ? Colors.white : Colors.black87,
() { () {
// 弹出重命名输入框 // 弹出重命名输入框
@ -282,7 +282,7 @@ class TranslationListView extends GetView<TranslationListController> {
size: 20.w, size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
), ),
'智能转写', 'smartTranscription'.tr, // 智能转写
isDarkMode ? Colors.white : Colors.black87, isDarkMode ? Colors.white : Colors.black87,
() { () {
Get.toNamed(Routes.meeting, arguments: { Get.toNamed(Routes.meeting, arguments: {
@ -296,7 +296,7 @@ class TranslationListView extends GetView<TranslationListController> {
size: 20.w, size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red, color: isDarkMode ? Colors.red[700] : Colors.red,
), ),
'删除', 'delete'.tr, // 删除
isDarkMode ? Colors.red[700]! : Colors.red, isDarkMode ? Colors.red[700]! : Colors.red,
() { () {
file.delete(); file.delete();
@ -358,7 +358,7 @@ class TranslationListView extends GetView<TranslationListController> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'重命名音频文件', 'renameAudioFile'.tr, // 重命名音频文件
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp, fontSize: 16.sp,
@ -390,7 +390,7 @@ class TranslationListView extends GetView<TranslationListController> {
fontSize: 16.sp, fontSize: 16.sp,
), ),
decoration: InputDecoration( decoration: InputDecoration(
hintText: '请输入新的文件名', hintText: 'enterNewFileName'.tr, // 请输入新的文件名
hintStyle: TextStyle( hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54, color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 16.sp, fontSize: 16.sp,
@ -422,7 +422,7 @@ class TranslationListView extends GetView<TranslationListController> {
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
), ),
child: Text( child: Text(
'取消', 'cancel'.tr, // 取消
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: isDarkMode ? AppColors.primary : Colors.blue, color: isDarkMode ? AppColors.primary : Colors.blue,
@ -455,7 +455,7 @@ class TranslationListView extends GetView<TranslationListController> {
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
), ),
child: Text( child: Text(
'确认', 'confirm'.tr, // 确认
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14.sp, fontSize: 14.sp,

21
lib/modules/translation/views/translation_view.dart

@ -154,7 +154,7 @@ class TranslationView extends GetView<TranslationController> {
size: 24.sp, size: 24.sp,
), ),
onPressed: controller.toggleisRecord, onPressed: controller.toggleisRecord,
tooltip: '是否开启录音', tooltip: 'enableRecording'.tr, // 是否开启录音
), ),
IconButton( IconButton(
icon: Icon( icon: Icon(
@ -167,7 +167,20 @@ class TranslationView extends GetView<TranslationController> {
Get.toNamed(Routes.translationList); Get.toNamed(Routes.translationList);
controller.setupScrollListener(); controller.setupScrollListener();
}, },
tooltip: '查看录音文件', 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, // 翻译历史
), ),
], ],
); );
@ -415,7 +428,7 @@ class TranslationView extends GetView<TranslationController> {
speakerId: 1, speakerId: 1,
activeSpeaker: controller.activeSpeaker.value, activeSpeaker: controller.activeSpeaker.value,
icon: Icons.headset, // 耳机图标 icon: Icons.headset, // 耳机图标
label: "耳机用户", label: "headsetUser".tr, // 耳机用户
), ),
SizedBox(width: 40.w), SizedBox(width: 40.w),
// 手机图标按钮 (说话者2) // 手机图标按钮 (说话者2)
@ -424,7 +437,7 @@ class TranslationView extends GetView<TranslationController> {
speakerId: 2, speakerId: 2,
activeSpeaker: controller.activeSpeaker.value, activeSpeaker: controller.activeSpeaker.value,
icon: Icons.phone_android, // 手机图标 icon: Icons.phone_android, // 手机图标
label: "手机用户", label: "phoneUser".tr, // 手机用户
), ),
], ],
); );

20
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -352,7 +352,6 @@ object AgentService : CoroutineScope {
*/ */
private fun startIdleCheck() { private fun startIdleCheck() {
stopIdleCheck() // 先停止现有的检查 stopIdleCheck() // 先停止现有的检查
Log.d(TAG, "启动空闲检测: ${isRecognitionActive}")
if (!isRecognitionActive) return if (!isRecognitionActive) return
idleCheckJob = launch { idleCheckJob = launch {
@ -360,7 +359,6 @@ object AgentService : CoroutineScope {
delay(maxIdleSeconds * 1000L) delay(maxIdleSeconds * 1000L)
if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) {
Log.d(TAG, "启动空闲检测: stopRecognition")
//stopRecognition() //stopRecognition()
sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds)) sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds))
} }
@ -398,14 +396,12 @@ object AgentService : CoroutineScope {
Log.d(TAG, "服务未初始化") Log.d(TAG, "服务未初始化")
return false return false
} }
Log.d(TAG, "麦克风音频未暂停,无法设置${azureAsrHelper!!.isPaused},$isExternalActive")
if(!(azureAsrHelper!!.isPaused)&&isExternalActive) if(!(azureAsrHelper!!.isPaused)&&isExternalActive)
{ {
Log.d(TAG, "麦克风音频未暂停,无法设置") Log.d(TAG, "麦克风音频未暂停,无法设置")
BleService.closeCodec() BleService.closeCodec()
return false return false
} }
Log.d(TAG, "开始语音识别")
if (isRecognitionActive) return true if (isRecognitionActive) return true
// 检查录音权限 // 检查录音权限
@ -499,14 +495,13 @@ object AgentService : CoroutineScope {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
Log.d(TAG, "onSessionStopped")
audioPlayer?.playAudio(R.raw.stop) audioPlayer?.playAudio(R.raw.stop)
} }
override fun onCanceled(reason: String, errorDetails: String) { override fun onCanceled(reason: String, errorDetails: String) {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
Log.d(TAG, "recognition_canceled")
BleService.closeCodec() BleService.closeCodec()
sendEvent("recognition_canceled", mapOf( sendEvent("recognition_canceled", mapOf(
"reason" to reason, "reason" to reason,
@ -517,7 +512,7 @@ object AgentService : CoroutineScope {
override fun onError(error: String) { override fun onError(error: String) {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
Log.d(TAG, "error")
BleService.closeCodec() BleService.closeCodec()
Log.e(TAG, "语音识别错误: $error") Log.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf( sendEvent("error", mapOf(
@ -549,7 +544,6 @@ object AgentService : CoroutineScope {
try { try {
azureAsrHelper?.stopContinuousRecognition() azureAsrHelper?.stopContinuousRecognition()
Log.d(TAG, "停止语音识别")
BleService.closeCodec() BleService.closeCodec()
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
@ -569,7 +563,7 @@ object AgentService : CoroutineScope {
try { try {
if(audioSourceType== AzureAsrHelper.AudioSourceType.EXTERNAL) if(audioSourceType== AzureAsrHelper.AudioSourceType.EXTERNAL)
{ {
Log.d(TAG, "当前是什么输入:$audioSourceType")
BleService.closeCodec() BleService.closeCodec()
stopRecognition() stopRecognition()
startRecognition(false) startRecognition(false)
@ -1447,12 +1441,12 @@ object AgentService : CoroutineScope {
// Log.d(TAG, "mcp调用结果: $metaStr") // Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片 if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music") val cardMusic = meta.getJSONObject("card_music")
val id = cardMusic.optInt("id", 0) val id = cardMusic.optLong("id", 0)
val url = cardMusic.optString("url", "") val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "") val name = cardMusic.optString("name", "")
val sgener = cardMusic.optString("sgener", "") val sgener = cardMusic.optString("sgener", "")
val image = cardMusic.optString("image", "") val image = cardMusic.optString("image", "")
val ts = cardMusic.optInt("ts", 0) val ts = cardMusic.optLong("ts", 0)
processMusicPlay( processMusicPlay(
mapOf( mapOf(
"id" to id, "id" to id,
@ -1470,12 +1464,12 @@ object AgentService : CoroutineScope {
val playlist = mutableListOf<Map<String, Any>>() val playlist = mutableListOf<Map<String, Any>>()
for (i in 0 until musics.length()) { for (i in 0 until musics.length()) {
val item = musics.optJSONObject(i) ?: continue val item = musics.optJSONObject(i) ?: continue
val id = item.optInt("id", 0) val id = item.optLong("id", 0)
val url = item.optString("url", "") val url = item.optString("url", "")
val name = item.optString("name", "") val name = item.optString("name", "")
val sgener = item.optString("sgener", "") val sgener = item.optString("sgener", "")
val image = item.optString("image", "") val image = item.optString("image", "")
val ts = item.optInt("ts", 0) val ts = item.optLong("ts", 0)
playlist.add( playlist.add(
mapOf( mapOf(
"id" to id, "id" to id,

18
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -458,9 +458,12 @@ class AzureAsrHelper(private val context: Context) {
EventHandler<SessionEventArgs> { _, _ -> EventHandler<SessionEventArgs> { _, _ ->
// 直接在当前线程调用回调 // 直接在当前线程调用回调
Log.d(tag, "会话结束事件") Log.d(tag, "会话结束事件")
if (audioSourceType == AudioSourceType.EXTERNAL) {
callback.onSessionStopped() callback.onSessionStopped()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopAudioProcessing() stopAudioProcessing()
}
} }
) )
@ -633,7 +636,12 @@ class AzureAsrHelper(private val context: Context) {
Log.d(tag, "关闭麦克风stopAudioPause: ") Log.d(tag, "关闭麦克风stopAudioPause: ")
// 清理旧的识别器 // 清理旧的识别器
isPaused = true isPaused = true
// 如果正在进行连续识别,先停止
if (isContinuousRecognitionActive) {
// 直接停止,不等待结果
recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false
}
} }
@ -642,9 +650,15 @@ class AzureAsrHelper(private val context: Context) {
*/ */
fun audioContinue() { fun audioContinue() {
Log.d(tag, "继续麦克风stopAudioPause: ") Log.d(tag, "继续麦克风stopAudioPause: ")
// 如果正在进行连续识别,先停止
if (!isContinuousRecognitionActive) {
// 直接停止,不等待结果
recognizer?.startContinuousRecognitionAsync()
isContinuousRecognitionActive = true
}
// 开始连续识别 // 开始连续识别
isPaused = false isPaused = false
} }
/** /**

46
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -38,7 +38,7 @@ class MusicService : MediaBrowserServiceCompat() {
private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION" private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION"
private val isRunning = AtomicBoolean(false) private val isRunning = AtomicBoolean(false)
private var currentIdInternal: String? = null private var currentIdInternal: Long? = null
private var currentStatus = false private var currentStatus = false
private var playlist = mutableListOf<Map<String,Any>>() private var playlist = mutableListOf<Map<String,Any>>()
private var currentIndex = 0 private var currentIndex = 0
@ -48,7 +48,7 @@ class MusicService : MediaBrowserServiceCompat() {
fun isServiceRunning() = isRunning.get() fun isServiceRunning() = isRunning.get()
fun isPlaying() = currentStatus fun isPlaying() = currentStatus
fun getCurrentId(): String? = currentIdInternal fun getCurrentId(): Long? = currentIdInternal
// // 获取播放进度信息 // // 获取播放进度信息
// fun getProgress(): Map<String, Any> { // fun getProgress(): Map<String, Any> {
@ -118,11 +118,11 @@ class MusicService : MediaBrowserServiceCompat() {
fun updateMediaSessionMetadata(song: Map<String, Any>) { fun updateMediaSessionMetadata(song: Map<String, Any>) {
instance?.let { service -> instance?.let { service ->
try { try {
val title = song["title"] as? String ?: song["name"] as? String ?: "未知标题" val title = song["title"] as? String ??: "未知标题"
val artist = song["artist"] as? String ?: song["sgener"] as? String ?: "未知歌手" val artist = song["artist"] as? String ??: "未知歌手"
val coverUrl = song["coverUrl"] as? String ?: song["image"] as? String ?: "" val coverUrl = song["coverUrl"] as? String ??: ""
currentIdInternal = song["id"] as? String ?: song["id"] as? String ?: "0" currentIdInternal = (song["id"] as? Number)?.toLong() ?: 0L
Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal") Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal")
val metadata = android.support.v4.media.MediaMetadataCompat.Builder() val metadata = android.support.v4.media.MediaMetadataCompat.Builder()
.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_TITLE, title) .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ARTIST, artist) .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
@ -216,13 +216,12 @@ private fun updateProgress() {
// 更新媒体会话进度 // 更新媒体会话进度
// 通知Flutter层 // 通知Flutter层
flutterChannel?.invokeMethod("onMediaSessionSeekTo1", flutterChannel?.invokeMethod("onMediaSessionProgress", mapOf(
mapOf(
"currentIdInternal" to currentIdInternal, "currentIdInternal" to currentIdInternal,
"currentPosition" to currentPosition, "currentPosition" to currentPosition,
"duration" to duration, "duration" to duration,
"progress" to progress "progress" to progress
)) ))
} }
} }
} }
@ -238,19 +237,20 @@ private fun updateProgress() {
when (command) { when (command) {
"play" -> intent.getSerializableExtra("song")?.let { url -> "play" -> intent.getSerializableExtra("song")?.let { url ->
val songMap = intent.getSerializableExtra("song") as? Map<String, Any> val songMap = intent.getSerializableExtra("song") as? Map<String, Any>
Log.i(TAG, "MusicePlay onStartCommand: $songMap")
songMap?.let { song -> songMap?.let { song ->
val songId = song["id"]as? String ?:"0" val songId = (song["id"] as? Number)?.toLong() ?: 0L
Log.i(TAG, "command songId: $songId")
currentIdInternal = songId currentIdInternal = songId
val existingIndex = playlist.indexOfFirst { it["id"] == songId } val existingIndex = playlist.indexOfFirst { it["id"] == songId}
if (existingIndex == -1) { if (existingIndex == -1) {
playlist.add(song) playlist.add(song)
currentIndex = playlist.size - 1 currentIndex = playlist.size - 1
} else { } else {
currentIndex = existingIndex currentIndex = existingIndex
} }
Log.i(TAG, "MusicePlay songId: $songId currentIndex:$currentIndex" )
play(song) play(song)
} }
} }
@ -546,8 +546,8 @@ private fun updateProgress() {
private fun play(song: Map<String,Any>) { private fun play(song: Map<String,Any>) {
val url = song["url"] as? String ?: return val url = song["url"] as? String ?: return
Log.i(TAG, "开始播放: $url") Log.i(TAG, "开始播放: $url")
val id = song["id"] as? String ?: "0" val id = (song["id"] as? Number)?.toLong() ?: 0L
Log.i(TAG, "开始播放play id: $id") Log.i(TAG, "开始播放play id: $id")
val title = song["title"] as? String ?: "未知标题" val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手" val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String val coverUrl = song["coverUrl"] as String
@ -586,8 +586,8 @@ private fun updateProgress() {
val title = song["title"] as? String ?: "未知标题" val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手" val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String val coverUrl = song["coverUrl"] as String
val id = song["id"] as? String ?: "0" val id =(song["id"] as? Number)?.toLong() ?: 0L
Log.i(TAG, "pause id: $id") Log.i(TAG, "pause id: $id")
currentIdInternal = id currentIdInternal = id
updateNotification(title,artist,coverUrl) updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED); setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
@ -607,8 +607,8 @@ private fun updateProgress() {
val title = song["title"] as? String ?: "未知标题" val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手" val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String val coverUrl = song["coverUrl"] as String
val id = song["id"] as? String ?: "0" val id = (song["id"] as? Number)?.toLong() ?: 0L
Log.i(TAG, "resume id: $id") Log.i(TAG, "resume id: $id")
currentIdInternal = id currentIdInternal = id
updateNotification(title,artist,coverUrl) updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PLAYING); setPlaybackState(PlaybackStateCompat.STATE_PLAYING);

11
local_plugins/music_service/lib/music_service.dart

@ -1,3 +1,5 @@
import 'dart:ffi';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class MusicService { class MusicService {
@ -101,7 +103,7 @@ class MusicService {
required Function() onPrevious, required Function() onPrevious,
required Function(int) onSeekTo, required Function(int) onSeekTo,
required Function() onStop, required Function() onStop,
required Function(String, int, int, double) onStop1, required Function(int, int, int, double) onProgress,
}) async { }) async {
// 设置方法调用处理器来处理来自原生平台的回调 // 设置方法调用处理器来处理来自原生平台的回调
_methodChannel.setMethodCallHandler((call) async { _methodChannel.setMethodCallHandler((call) async {
@ -125,13 +127,12 @@ class MusicService {
case 'onMediaSessionStop': case 'onMediaSessionStop':
onStop(); onStop();
break; break;
case 'onMediaSessionSeekTo1': case 'onMediaSessionProgress':
final currentIdInternal = final currentIdInternal = call.arguments['currentIdInternal'] as int;
call.arguments['currentIdInternal'] as String;
final currentPosition = call.arguments['currentPosition'] as int; final currentPosition = call.arguments['currentPosition'] as int;
final duration = call.arguments['duration'] as int; final duration = call.arguments['duration'] as int;
final progress = call.arguments['progress'] as double; final progress = call.arguments['progress'] as double;
onStop1(currentIdInternal, currentPosition, duration, progress); onProgress(currentIdInternal, currentPosition, duration, progress);
break; break;
} }
}); });

8
local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt

@ -337,10 +337,10 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
BleService.isEnterOta = false BleService.isEnterOta = false
isDisconnect = false isDisconnect = false
if(otaManager!= null) { if(otaManager!= null) {
BleService.removeCallback(this)
otaManager?.bluetoothHelper?.stopScan() otaManager?.release()
otaManager= null
otaManager?.bluetoothHelper?.disconnectDevice(otaDevice) bluetoothHelper=null
} }
result.success("") result.success("")
} else { } else {

Loading…
Cancel
Save