diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index b916bc88d..d532f449e 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/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 '../../data/services/speech_factory.dart'; import '../../data/services/volcano_translation_service.dart'; +import '../../data/services/volcano_ai_service.dart'; import '../../data/services/language_manager.dart'; import '../../core/utils/logger.dart'; import '../../data/services/bluetooth_manager.dart'; @@ -40,7 +41,7 @@ class InitialBinding extends Bindings { Get.lazyPut(() => NavigationManager(), fenix: true); Get.lazyPut(() => LocationManager(), fenix: true); // 火山AI服务 - // Get.lazyPut(() => VolcanoAIService(), fenix: true); + Get.lazyPut(() => VolcanoAIService(), fenix: true); // 账号管理服务 Get.lazyPut(() => AccountService(), fenix: true); diff --git a/lib/core/themes/language/lang/en_us.dart b/lib/core/themes/language/lang/en_us.dart deleted file mode 100644 index e4c912e19..000000000 --- a/lib/core/themes/language/lang/en_us.dart +++ /dev/null @@ -1,95 +0,0 @@ -const Map 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" -}; diff --git a/lib/core/themes/language/lang/zh_cn.dart b/lib/core/themes/language/lang/zh_cn.dart deleted file mode 100644 index 604f31448..000000000 --- a/lib/core/themes/language/lang/zh_cn.dart +++ /dev/null @@ -1,93 +0,0 @@ -const Map 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": "录音笔" -}; diff --git a/lib/core/themes/language/translation_service.dart b/lib/core/themes/language/translation_service.dart deleted file mode 100644 index 6ca44bda0..000000000 --- a/lib/core/themes/language/translation_service.dart +++ /dev/null @@ -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> get keys => {'en_US': enUS, 'zh_CN': zhCN}; -} diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 43063954b..5b6f8624e 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -364,11 +364,85 @@ const Map zhCN = { "healthAdvisorWelcome": "您好,我是吴小江,很高兴能为您提供多功能AI聊天。我将用专业知识帮助您。请问有什么问题想咨询吗?", "healthAdvisorVoiceWelcome": "我在这里!", - // 翻译历史 + // 翻译模块 + // 翻译主页 (translation_view.dart) + "translationHistory": "翻译历史", + "enableRecording": "是否开启录音", + "viewAudioFiles": "查看录音文件", + "headsetUser": "耳机用户", + "phoneUser": "手机用户", + + // 翻译历史页 (translation_history_view.dart) "noTranslationHistory": "暂无历史记录", "clearHistory": "清除历史记录", "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": "用户反馈", diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 1c921fa07..fcd6450ed 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -472,13 +472,37 @@ class BleManager extends GetxService { firmwareProductColor: '', ); } - - // 并行获取所有设备信息,不使用延迟 - final results = await Future.wait([ - _bleService.getBatteryInfo(), - _bleService.getDeviceVersion(), - _bleService.getProductId(), - ]); + final commands = [ + _bleService.getBatteryInfo, + _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( '设备信息获取结果: 电池=${results[0]}, 版本=${results[1]}, 产品ID=${results[2]}'); diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index 621cd927c..87a05df40 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -105,7 +105,7 @@ class MusicManager extends GetxService { required Function() onPrevious, required Function(int) onSeekTo, required Function() onStop, - required Function(String, int, int, double) onStop1, + required Function(int, int, int, double) onProgress, }) async { try { await _musicService.setMediaSessionCallbacks( @@ -115,7 +115,7 @@ class MusicManager extends GetxService { onPrevious: onPrevious, onSeekTo: onSeekTo, onStop: onStop, - onStop1: onStop1, + onProgress: onProgress, ); } catch (e) { Logger.d('MusicManager', '设置媒体会话回调失败: $e'); diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 6c69ad897..ab76f6703 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:amap_service/amap_service.dart'; import 'package:deep_voice/core/utils/upload_oss.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/navigation_manager.dart'; import 'package:deep_voice/data/services/network/api.dart'; @@ -121,16 +120,23 @@ class AgentController extends GetxController { // 会话ID 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 RxBool isLoadingMusic = false.obs; - final RxString loadingMusicId = ''.obs; + final RxInt loadingMusicId = 0.obs; // 设置加载状态的方法 - void setLoadingMusic(String id, bool loading) { - loadingMusicId.value = loading ? id : ''; + void setLoadingMusic(int id, bool loading) { + loadingMusicId.value = loading ? id : 0; isLoadingMusic.value = loading; } @@ -138,6 +144,7 @@ class AgentController extends GetxController { void onInit() { super.onInit(); _loadTtsPreference(); //加载TTS偏好设置 + _setupScrollListener(); //设置滚动监听器 _loadChatHistory(); //加载聊天历史记录 _subscribeToEvents(); //订阅代理服务事件 _initMusicStatus(); //初始化音乐状态 @@ -166,7 +173,7 @@ class AgentController extends GetxController { onPrevious: onMediaSessionPrevious, onSeekTo: onMediaSessionSeekTo, onStop: onMediaSessionStop, - onStop1: onMediaSessionSeekTo1, + onProgress: onMediaSessionProgress, ); Logger.d('AgentController', '媒体会话回调设置完成'); } catch (e) { @@ -271,63 +278,33 @@ class AgentController extends GetxController { final result = await ChatStorage.getMessages( sessionId: sessionId, //会话ID page: 1, //从第一页开始 - pageSize: 20, //每页20条记录 + pageSize: pageSize, //每页20条记录 ); if (result.messages.isNotEmpty) { // 转换并添加消息到列表 - final historyMessages = result.messages.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(); + final historyMessages = _convertChatMessagesToMessages(result.messages); // 消息按时间排序,最新的消息在底部 historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); messages.assignAll(historyMessages); - Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录'); - // 详细打印每条消息的内容和属性,便于调试 - Logger.d(TAG, '===== 历史消息详情开始 ====='); - for (int i = 0; i < historyMessages.length; i++) { - final msg = historyMessages[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, '===== 历史消息详情结束 ====='); + // 更新分页状态 + currentPage.value = 1; + hasMoreMessages.value = result.totalPages > 1; + + Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); + _logMessageDetails(historyMessages); } else { // 没有历史记录时添加欢迎消息 _addWelcomeMessage(); + hasMoreMessages.value = false; Logger.i(TAG, '没有历史记录,添加欢迎消息'); } } catch (e) { Logger.e(TAG, '加载聊天历史失败: $e'); _addWelcomeMessage(); + hasMoreMessages.value = false; } finally { isHistoryLoaded.value = true; messages.refresh(); @@ -335,6 +312,137 @@ class AgentController extends GetxController { } } + // 加载更多历史消息(分页加载) + Future 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 _convertChatMessagesToMessages(List 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 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() { messages.add(Message( @@ -1308,7 +1416,6 @@ class AgentController extends GetxController { // 设置按住说话状态 isPushToTalkActive.value = true; - print("开始按住说话,状态已设置为true"); // 开始语音识别 await AgentService.audioContinue(); // await startVoiceInput(); @@ -1333,7 +1440,7 @@ class AgentController extends GetxController { try { var status = await _musiceManager.getStatus(); isPlaying.value = status['isPlaying'] ?? false; - currentId.value = status['id'] ?? 0; + currentId.value = status['id'] as int; Logger.d('AgentController', '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); print("音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}"); @@ -1351,16 +1458,14 @@ class AgentController extends GetxController { isPlaying.value = true; // 修复:确保currentId接收int类型 final songId = song['id']; - currentId.value = songId is int - ? songId - : (int.tryParse(songId?.toString() ?? '0') ?? 0); + currentId.value = songId ?? 0; // 重置进度状态 currentPosition.value = 0; duration.value = 0; progress.value = 0.0; - final id = song['id'] ?? ""; + final id = song['id'] ?? 0; final timestampMs = song['ts'] ?? 0; // 检查URL是否需要刷新(只有当有时间戳时才检查) @@ -1382,9 +1487,7 @@ class AgentController extends GetxController { song["url"] = urldata['url'] ?? ''; // 修复:确保currentId接收int类型 final newSongId = song['id']; - currentId.value = newSongId is int - ? newSongId - : (int.tryParse(newSongId?.toString() ?? '0') ?? 0); + currentId.value = newSongId ?? 0; Logger.d(TAG, '获取新URL成功'); } catch (e) { Logger.e(TAG, '获取新URL失败: $e'); @@ -1394,8 +1497,8 @@ class AgentController extends GetxController { } // 转换为MusicManager期望的格式 - final musicData = { - 'id': song['id']?.toString() ?? '', + final musicData = { + 'id': song['id'], 'url': song['url']?.toString() ?? '', 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'artist': @@ -1411,9 +1514,6 @@ class AgentController extends GetxController { await _updateMediaSessionMetadata(musicData); await _updateMediaSessionPlaybackState(true); - // 启动进度定时器 - _startProgressTimer(); - Logger.d(TAG, '音乐播放启动成功'); } catch (e) { Logger.e(TAG, '播放音乐失败: $e'); @@ -1444,7 +1544,6 @@ class AgentController extends GetxController { Logger.d('AgentController', '恢复播放音乐'); await _musiceManager.resume(); isPlaying.value = true; - _startProgressTimer(); // 更新媒体会话播放状态 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() { if (_progressTimer != null) { @@ -1483,61 +1565,17 @@ class AgentController extends GetxController { } // 获取播放进度 - Future updateMusicProgress() async { - 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 updateMusicProgress1(String currentIdInternal, int newPosition, + Future updateMusicProgress(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { - if (int.parse(currentIdInternal) != 0) { + if (currentIdInternal != 0) { isPlaying.value = true; // 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; - if (currentId.value != int.parse(currentIdInternal)) { - currentId.value = int.parse(currentIdInternal); + if (currentId.value != currentIdInternal) { + currentId.value = currentIdInternal; hasUpdate = true; } // 只有当值发生变化时才更新,避免不必要的UI刷新 @@ -1602,20 +1640,17 @@ class AgentController extends GetxController { Future seekToPosition(int positionMs) async { try { await _musiceManager.seekTo(positionMs); - // 立即更新进度状态 - // await updateMusicProgress(); } catch (e) { Logger.e('AgentController', '跳转播放位置失败: $e'); } } // 跳转到指定位置 - Future seekToPosition1(String currentIdInternal, int newPosition, + Future seekToProgress(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { - //await _musiceManager.seekTo(positionMs); // 立即更新进度状态 - await updateMusicProgress1( + await updateMusicProgress( currentIdInternal, newPosition, newDuration, newProgress); } catch (e) { Logger.e('AgentController', '跳转播放位置失败: $e'); @@ -1676,7 +1711,7 @@ class AgentController extends GetxController { // 设置加载状态 isLoadingMusic.value = true; - loadingMusicId.value = playlist[0]['id']?.toString() ?? ''; + loadingMusicId.value = playlist[0]['id']; // 重置播放状态 isPlaying.value = true; @@ -1687,7 +1722,7 @@ class AgentController extends GetxController { // 格式化歌曲列表数据 final formattedPlaylist = playlist.map((song) { return { - 'id': song['id']?.toString() ?? '', + 'id': song['id'], 'url': song['url']?.toString() ?? '', 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'artist': @@ -1702,12 +1737,9 @@ class AgentController extends GetxController { // 更新当前播放歌曲ID if (formattedPlaylist.isNotEmpty) { - currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0; + currentId.value = formattedPlaylist[0]['id'] ?? 0; } - // 启动进度定时器 - _startProgressTimer(); - Logger.d(TAG, '音乐列表播放启动成功'); } catch (e) { Logger.e(TAG, '播放音乐列表失败: $e'); @@ -1726,7 +1758,7 @@ class AgentController extends GetxController { } finally { // 清除加载状态 isLoadingMusic.value = false; - loadingMusicId.value = ''; + loadingMusicId.value = 0; } } @@ -1778,7 +1810,7 @@ class AgentController extends GetxController { // 查找当前歌曲在列表中的索引 int index = -1; for (int i = 0; i < playlist.length; i++) { - if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) { + if (playlist[i]['id'] == songId) { index = i; break; } @@ -1836,12 +1868,12 @@ class AgentController extends GetxController { _stopProgressTimer(); } - /// 处理来自系统通知栏的跳转操作 - Future onMediaSessionSeekTo1(String currentIdInternal, int newPosition, + /// 处理来自系统进度条 + Future onMediaSessionProgress(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { - print( - "onMediaSessionSeekTo1 currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); - await seekToPosition1( + // print( + // "onMediaSessionProgress currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); + await seekToProgress( currentIdInternal, newPosition, newDuration, newProgress); } } diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 013166031..703aded40 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -200,8 +200,13 @@ class _AgentViewState extends State with WidgetsBindingObserver { reverse: true, padding: const EdgeInsets.symmetric( vertical: 16, horizontal: 16), - itemCount: controller.messages.length, + itemCount: controller.messages.length + (controller.hasMoreMessages.value ? 1 : 0), itemBuilder: (context, index) { + // 如果是最后一项且有更多消息,显示加载指示器 + if (index == controller.messages.length && controller.hasMoreMessages.value) { + return _buildLoadMoreIndicator(); + } + final message = controller .messages[controller.messages.length - 1 - index]; return Padding( @@ -1122,4 +1127,80 @@ class _AgentViewState extends State 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, + ), + ), + ), + ); + } + }); + } } diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart index f4af0c4fb..e00cb158a 100644 --- a/lib/modules/agent/views/music_card.dart +++ b/lib/modules/agent/views/music_card.dart @@ -200,7 +200,7 @@ Widget buildMusicContent( ...playlist.asMap().entries.take(4).map((entry) { final index = entry.key; final item = entry.value; - final id = item["id"] ?? 0; + final id = item["id"]; final name = item["name"] ?? "未知歌曲"; final url = item["url"] ?? ""; final singer = item["sgener"] ?? "未知歌手"; diff --git a/lib/modules/agent/widgets/sound_bar_widget.dart b/lib/modules/agent/widgets/sound_bar_widget.dart index e5c483b36..c108bcca2 100644 --- a/lib/modules/agent/widgets/sound_bar_widget.dart +++ b/lib/modules/agent/widgets/sound_bar_widget.dart @@ -134,8 +134,10 @@ class AgentSoundBarsWidget extends StatelessWidget { Widget build(BuildContext context) { return Obx(() { final controller = Get.find(); + final shouldShowAnimation = + controller.isListening.value || controller.isPushToTalkActive.value; return SoundBarsWidget( - isActive: controller.isListening.value, + isActive: shouldShowAnimation, barCount: barCount, ); }); diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index 69fa15a85..ec63a75c2 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -1,8 +1,6 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter/services.dart'; - import '../../../core/utils/logger.dart'; import '../../agent/controllers/agent_controller.dart'; import '../../agent/views/circular_progress_button.dart'; @@ -80,12 +78,11 @@ class MusicPlaylistView extends StatelessWidget { if (controller.currentId.value != 0) { try { currentSong = playlist.firstWhere( - (song) => - int.tryParse(song['id']?.toString() ?? '0') == - controller.currentId.value, + (song) => song['id'] == controller.currentId.value, ); } catch (e) { // 如果没找到,使用第一首歌曲 + currentSong = playlist.isNotEmpty ? playlist.first : null; } } else { @@ -192,7 +189,7 @@ class MusicPlaylistView extends StatelessWidget { itemCount: playlist.length, itemBuilder: (context, index) { final item = playlist[index]; - final id = item["id"]?.toString() ?? ""; + final id = item["id"] ?? 0; final name = item["name"] ?? "未知歌曲"; final url = item["url"] ?? ""; final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; @@ -215,7 +212,7 @@ class MusicPlaylistView extends StatelessWidget { Widget _buildMusicItem( BuildContext context, AgentController controller, - String id, + int id, String name, String url, String singer, @@ -323,7 +320,7 @@ class MusicPlaylistView extends StatelessWidget { SizedBox(width: 14.w), // 播放按钮 Obx(() { - final currentIdInt = int.tryParse(id) ?? 0; + final currentIdInt = id; final isThisPlaying = controller.isPlaying.value && controller.currentId.value == currentIdInt; final currentProgress = @@ -601,7 +598,7 @@ class MusicPlaylistView extends StatelessWidget { // 播放选中的歌曲 void _playSelectedSong(AgentController controller, Map song, List> playlist) { - final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0; + final songId = song['id'] ?? 0; controller.updateCurrentPlaylistContext(playlist, songId); controller.musicPlay(song); } diff --git a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart index 8245d4096..eb4c64e3c 100644 --- a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart +++ b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart @@ -89,33 +89,17 @@ class OtaUpgradeView extends GetView { ), body: SingleChildScrollView( child: Padding( - padding: EdgeInsets.all(20), + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), child: Column( mainAxisSize: MainAxisSize.min, children: [ - // 设备信息卡片 - Card( - elevation: 2, - 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), + // 设备信息卡片 - 优化后的版本 + _buildDeviceCard(isDarkMode), + SizedBox(height: 24.h), - // 更新说明 - _buildUpdateNotes(isDarkMode), - SizedBox(height: 20), + // 更新说明 - 当有可用更新时显示 + //_buildUpdateNotes(isDarkMode), + SizedBox(height: 16.h), // 状态显示区域 Obx(() { @@ -135,257 +119,696 @@ class OtaUpgradeView extends GetView { ); } + // 设备信息卡片 + 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 ? 'assets/images/headphone_dark.png' : 'assets/images/headphone_light.png', - height: 180.h, + height: 120.h, fit: BoxFit.contain, - ); + ), + ); + } - Widget _buildVersionInfo(bool isDarkMode) => Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + // 版本信息显示 + Widget _buildVersionInfo(bool isDarkMode) { + 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: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, // 改为center对齐 + Text( + 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: [ - Text('当前固件版本:'.tr, - style: TextStyle( - fontSize: 18, - color: isDarkMode ? Colors.grey[300] : Colors.black)), - SizedBox(width: 8), // 改为width - Obx(() => Text('V ${controller.currentVersion}', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.normal, - color: isDarkMode ? Colors.blue[300] : Colors.blue))), + Icon( + Icons.update, + size: 16.sp, + color: Colors.orange, + ), + SizedBox(width: 6.w), + Text( + '发现新版本', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.orange, + ), + ), ], ), - SizedBox(height: 8), // 改为height - Row( - crossAxisAlignment: CrossAxisAlignment.center, // 改为center对齐 + ); + } else { + 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: [ - Text('最新固件版本:'.tr, - style: TextStyle( - fontSize: 18, - color: isDarkMode ? Colors.grey[300] : Colors.black)), - SizedBox(width: 8), // 改为width - Obx(() => Text('V ${controller.latestVersion}', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.normal, - color: isDarkMode ? Colors.blue[300] : Colors.blue))), + Icon( + Icons.check_circle, + size: 16.sp, + color: Colors.green, + ), + SizedBox(width: 6.w), + Text( + '已是最新版本', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.green, + ), + ), ], ), - ], - ); + ); + } + }); + } - // 添加固件更新说明 - Widget _buildUpdateNotes(bool isDarkMode) => - Obx(() => controller.isUpdateAvailable.value - ? Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + // 固件更新说明 + Widget _buildUpdateNotes(bool isDarkMode) { + return Obx(() => controller.isUpdateAvailable.value && + controller.updatedescription.value.isNotEmpty + ? Container( + width: double.infinity, + 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, - child: Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( + borderRadius: BorderRadius.circular(16.r), + border: Border.all( + color: isDarkMode + ? Colors.amber[700]!.withValues(alpha: 0.3) + : Colors.amber[200]!.withValues(alpha: 0.5), + width: 1, + ), + ), + 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, - color: isDarkMode ? Colors.amber[300] : Colors.amber, - size: 20, + color: isDarkMode ? Colors.amber[300] : Colors.amber[700], + size: 18.sp, ), - SizedBox(width: 8), - Text( - '更新内容'.tr, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black, - ), + ), + SizedBox(width: 12.w), + Text( + '更新内容'.tr, + style: TextStyle( + fontSize: 16.sp, + 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(), - SizedBox(height: 8), - // 这里可以从controller中获取更新说明,如果有的话 - Text( + child: Text( controller.updatedescription.value, style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[300] : Colors.grey[800], + fontSize: 14.sp, + 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: [ - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - isDarkMode ? Colors.blue[300] : Colors.blue), + // 自定义加载动画 + SizedBox( + 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), - decoration: BoxDecoration( - color: - isDarkMode ? Colors.red[900]!.withOpacity(0.2) : Colors.red[50], - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isDarkMode ? Colors.red[700]! : Colors.red[200]!, - width: 1, - ), + // 错误信息显示 + Widget _buildErrorInfo(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(24.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + 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( - children: [ - Icon(Icons.error_outline, color: Colors.red, size: 40), - SizedBox(height: 10), - Text(controller.errorMessage.value, - textAlign: TextAlign.center, + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: isDarkMode + ? Colors.red[700]!.withValues(alpha: 0.3) + : Colors.red[200]!.withValues(alpha: 0.5), + 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( - color: isDarkMode ? Colors.red[300] : Colors.red, - fontSize: 16)), - SizedBox(height: 20), - ElevatedButton.icon( - icon: Icon(Icons.refresh), - label: Text('重试连接'.tr), + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), style: ElevatedButton.styleFrom( - backgroundColor: isDarkMode ? Colors.blue[700] : Colors.blue, + backgroundColor: isDarkMode ? const Color(0xFF0066CC) : Colors.blue, foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + padding: EdgeInsets.symmetric(vertical: 14.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.r), ), + elevation: 0, ), onPressed: controller.retryConnection, - ) - ], - ), - ); + ), + ), + ], + ), + ); + } + // 更新按钮 Widget _buildUpdateButton(bool isDarkMode) { - return controller.isUpgrade.value - ? _buildProgressIndicator(isDarkMode) - : Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 10), - child: Column( - children: [ - ElevatedButton.icon( - icon: Icon( - controller.isUpdateAvailable.value - ? Icons.system_update - : Icons.check_circle, + return Obx(() { + if (controller.isUpgrade.value) { + return _buildProgressIndicator(isDarkMode); + } + + return SizedBox( + width: double.infinity, + child: Column( + children: [ + // 主要更新按钮 + Container( + 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: TextStyle(fontSize: 16), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), ), - style: ElevatedButton.styleFrom( - backgroundColor: controller.isUpdateAvailable.value - ? (isDarkMode ? Colors.blue[700] : Colors.blue) - : (isDarkMode ? Colors.grey[800] : Colors.grey), - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - minimumSize: Size(240, 50), + ), + onPressed: controller.isUpdateAvailable.value + ? controller.startUpdate + : null, + ), + ), + + // 更新提示信息 + if (controller.isUpdateAvailable.value) ...[ + 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) ...[ - SizedBox(height: 8), - Text( - '取出双耳机,并保证电量均为30%以上的状态'.tr, - style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Colors.orange, + size: 20.sp, ), - ), - ], - ], - ), - ); + 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) { return Obx(() { final progress = controller.progress.value; final type = controller.type.value; - // // 处理升级失败状态:弹出 snackbar 并显示错误提示 - // 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], - // ), - // ), - // ); - // } + final statusMessage = controller.getStatusMessage(type); - // 正常升级状态 return Container( 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( children: [ - Text( - controller.getStatusMessage(type), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.blue[300] : Colors.blue[700], - ), + // 状态图标和文字 + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + 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), - LinearProgressIndicator( - value: progress / 100.0, // 转换为 0.0-1.0 - backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200], - valueColor: AlwaysStoppedAnimation( - isDarkMode ? Colors.blue[300]! : Colors.blue, + SizedBox(height: 24.h), + + // 进度条容器 + Container( + height: 8.h, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.1) + : Colors.grey[200], + borderRadius: BorderRadius.circular(4.r), ), - ), - const SizedBox(height: 8), - Text( - '${progress.toStringAsFixed(1)}%', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.blue[300] : Colors.blue[700], + child: ClipRRect( + borderRadius: BorderRadius.circular(4.r), + child: LinearProgressIndicator( + value: progress / 100.0, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + isDarkMode ? const Color(0xFF0066CC) : Colors.blue, + ), + ), ), ), + 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], + ), + ), + ], + ), ], ), ); diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index 98c0da59d..e2c57a222 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -305,44 +305,44 @@ class PairingView extends GetView { String _getSubtitleForState(PairingState state) { switch (state) { case PairingState.idle: - return '准备开始配对'; + return 'readyToPair'.tr; // 准备开始配对 case PairingState.checking: - return '正在准备配对环境...'; + return 'preparingPairingEnvironment'.tr; // 正在准备配对环境... case PairingState.waitingClassic: - return '请先通过系统蓝牙连接DeapSound设备'; + return 'connectDeepSoundViaSystemBluetooth'.tr; // 请先通过系统蓝牙连接DeapSound设备 case PairingState.verifying: - return '正在验证设备...'; + return 'verifyingDevice'.tr; // 正在验证设备... case PairingState.binding: - return '设备绑定确认...'; + return 'deviceBindingConfirm'.tr; // 设备绑定确认... case PairingState.associating: - return '正在建立设备关联...'; + return 'establishingDeviceAssociation'.tr; // 正在建立设备关联... case PairingState.waitingBle: - return '请稍候,正在完成配对...'; + return 'waitingCompletePairing'.tr; // 请稍候,正在完成配对... case PairingState.completed: - return '配对成功!即将跳转到主页...'; + return 'pairingSuccessRedirecting'.tr; // 配对成功!即将跳转到主页... case PairingState.error: final errorType = controller.errorType; switch (errorType) { case ErrorType.bluetoothOff: - return '请开启蓝牙后重试'; + return 'bluetoothTurnedOff'.tr; // 蓝牙已关闭,请打开蓝牙 case ErrorType.noPermission: - return '需要蓝牙权限才能继续'; + return 'lackPermissions'.tr; // 缺少蓝牙权限 case ErrorType.noClassicDevice: - return '请先通过系统蓝牙连接DeapSound设备'; + return 'connectDeepSoundViaSystemBluetooth'.tr; // 请先通过系统蓝牙连接DeapSound设备 case ErrorType.deviceNotMatch: - return '请确保连接的是DeapSound设备'; + return 'ensureConnectedDeepSoundDevice'.tr; // 请确保连接的是DeapSound设备 case ErrorType.deviceBound: - return '该设备已被其他账号绑定'; + return 'deviceAlreadyBoundToOtherAccount'.tr; // 该设备已被其他账号绑定 case ErrorType.associationFailed: - return '设备关联失败,请重试'; + return 'deviceAssociationFailed'.tr; // 设备关联失败,请重试 case ErrorType.bleTimeout: - return '扫描超时,请确保设备在附近'; + return 'scanTimeoutEnsureDeviceNearby'.tr; // 扫描超时,请确保设备在附近 case ErrorType.networkError: - return '网络错误,请检查网络连接'; + return 'networkError'.tr; // 网络错误 case ErrorType.authnFailed: - return '授权失败,请检查授权码是否正确'; + return 'authorizationFailedCheckCode'.tr; // 授权失败,请检查授权码是否正确 default: - return '配对失败,请重试'; + return 'pairingFailedRetry'.tr; // 配对失败,请重试 } default: return ''; @@ -489,7 +489,7 @@ class PairingView extends GetView { minimumSize: Size(double.infinity, 56.h), ), child: Text( - "绑定", + "bind".tr, // 绑定 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -530,7 +530,7 @@ class PairingView extends GetView { controller: controller .codeInputController, // 你可以在 Controller 中声明这个 decoration: InputDecoration( - hintText: '请输入代码', + hintText: 'enterCode'.tr, // 请输入代码 border: OutlineInputBorder( borderRadius: BorderRadius.circular(12.r), ), @@ -552,7 +552,7 @@ class PairingView extends GetView { elevation: 0, ), child: Text( - '授权绑定', + 'authorizeBind'.tr, // 授权绑定 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -596,7 +596,7 @@ class PairingView extends GetView { minimumSize: Size(double.infinity, 48.h), ), child: Text( - '跳过绑定'.tr, // 稍后再说 + 'skipBinding'.tr, // 跳过绑定 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, @@ -615,7 +615,7 @@ class PairingView extends GetView { minimumSize: Size(double.infinity, 48.h), ), child: Text( - '重新扫描'.tr, // 稍后再说 + 'rescan'.tr, // 重新扫描 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, diff --git a/lib/modules/pairing/views/pairing_view_new.dart b/lib/modules/pairing/views/pairing_view_new.dart index 59026841f..2df7c4435 100644 --- a/lib/modules/pairing/views/pairing_view_new.dart +++ b/lib/modules/pairing/views/pairing_view_new.dart @@ -45,7 +45,7 @@ class PairingView extends GetView { return Column( children: [ Text( - '设备配对', + 'devicePairing'.tr, // 设备配对 style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.bold, @@ -54,7 +54,7 @@ class PairingView extends GetView { ), SizedBox(height: 8.h), Text( - '连接您的 DeepSound 耳机', + 'connectYourDeepSoundHeadset'.tr, // 连接您的 DeepSound 耳机 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -208,7 +208,7 @@ class PairingView extends GetView { // 主要操作按钮 if (showConnectButton) _buildPrimaryButton( - '连接设备', + 'connectDevice'.tr, // 连接设备 controller.onConnectTap, isDarkMode, ), @@ -218,7 +218,7 @@ class PairingView extends GetView { TextButton( onPressed: controller.confirmBindDevice, child: Text( - '绑定', + 'bind'.tr, // 绑定 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -228,7 +228,7 @@ class PairingView extends GetView { if (showRetryButton) _buildPrimaryButton( - '重试', + 'retry'.tr, // 重试 controller.onRetryTap, isDarkMode, ), @@ -250,7 +250,7 @@ class PairingView extends GetView { elevation: 0, ), child: Text( - '自动输入代码', + 'autoInputCode'.tr, // 自动输入代码 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -265,7 +265,7 @@ class PairingView extends GetView { TextButton( onPressed: controller.onSkipTap, child: Text( - '跳过', + 'skip'.tr, // 跳过 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 7c69a9216..d8c0976b2 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -841,7 +841,7 @@ class SettingsView extends GetView { //------------测试使用标记 start---------------// SizedBox(height: 4.h), Text( - '${Platform.operatingSystem}-test-0626', + '${Platform.operatingSystem}-test-0707', style: TextStyle( fontSize: 12.sp, color: const Color.fromARGB(255, 143, 94, 94), diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 924bfa972..4f12b8d40 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/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:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import '../../../data/services/music_manager.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/tts_service.dart'; import '../../../data/services/language_manager.dart'; @@ -35,6 +36,8 @@ class TranslationController extends GetxController { final GetStorage _storage = GetStorage(); // 蓝牙服务 final bleManager = Get.find(); + //音乐播放管理 + final _musiceManager = Get.find(); // 音频输入源 bool _audioSourceType = false; //是否开启播放 @@ -45,14 +48,20 @@ class TranslationController extends GetxController { Timer? _recordTimer; //秒 int _seconds = 0; - // 存储相关 + // 存储相关配置 static const String _historyKey = 'translation_history'; static const String _sourceLanguageKey = 'translation_source_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 lastActiveSpeaker = 0.obs; // 上一次状态 + bool isContinuePlaying = false; // 是否继续播放 // 状态变量 final isRecognizing = false.obs; final isTranslating = false.obs; @@ -172,7 +181,7 @@ class TranslationController extends GetxController { // 初始化服务 _initServices(); - + suspendOtherServices(); // 使用微任务在布局完成后自动滚动到底部 WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToBottom(); @@ -247,6 +256,32 @@ class TranslationController extends GetxController { } } + // 暂停其他服务 + Future 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 restoreOtherServices() async { + try { + if (isContinuePlaying) await _musiceManager.resume(); // 暂停音乐 + } catch (e) { + Logger.error('初始化服务失败: ${e.toString()}'); + } + } + // 初始化服务 Future _initServices() async { try { @@ -285,6 +320,7 @@ class TranslationController extends GetxController { _voiceInteractionSubscription?.cancel(); _translationDebounceTimer?.cancel(); scrollController.dispose(); + restoreOtherServices(); super.onClose(); } @@ -559,8 +595,8 @@ class TranslationController extends GetxController { String? translationResult; try { - var detectedSourceLanguageCode; - var detectedTargetLanguageCode; + String detectedSourceLanguageCode; + String detectedTargetLanguageCode; if ((currentMode.value == 'faceToFace' && activeSpeaker.value == 1) || (currentMode.value == 'faceToFace' && activeSpeaker.value == 0 && @@ -608,7 +644,12 @@ class TranslationController extends GetxController { translationHistory[index].targetLanguageCode = detectedTargetLanguageCode; translationHistory.refresh(); - saveTranslationHistory(); + + // 【改进】只有在最终结果时才保存,避免中间态时的频繁保存 + if (isFinal) { + saveTranslationHistory(); + } + // 更新后滚动到底部 _scrollToBottom(); // 播放TTS - 只在最终结果时播放 @@ -629,8 +670,7 @@ class TranslationController extends GetxController { } } } else { - Logger.info( - '未找到匹配的历史项 ${sourceText} ${timestamp.toIso8601String() ?? "无时间戳"}'); + Logger.info('未找到匹配的历史项 $sourceText ${timestamp.toIso8601String()}'); } } } catch (e) { @@ -836,10 +876,13 @@ class TranslationController extends GetxController { final List allHistory = historyList.map((item) => TranslationItem.fromJson(item)).toList(); - // 只保留最新的10条记录 - if (allHistory.length > _maxHistoryItems) { + // 【注意】存储中的记录都是完成态记录(isIntermediate = false) + // 因为保存时已经过滤掉了中间态记录,确保数据质量 + + // 主页面只显示最近的20条完成态记录,保证界面性能 + if (allHistory.length > _maxDisplayItems) { translationHistory.value = - allHistory.sublist(allHistory.length - _maxHistoryItems); + allHistory.sublist(allHistory.length - _maxDisplayItems); } else { translationHistory.value = allHistory; } @@ -855,38 +898,170 @@ class TranslationController extends GetxController { } } - // 保存翻译历史 + // 保存翻译历史 - 回归原始简单实现 + // 【修复】完全回归原始保存逻辑,只保存当前内存中的完成态记录 void saveTranslationHistory() { - if (translationHistory.length > _maxHistoryItems) { - translationHistory.removeRange( - 0, translationHistory.length - _maxHistoryItems); - } - try { + // 只保存当前内存中的完成态记录,过滤掉中间态记录 + final List finalRecords = + translationHistory.where((item) => !item.isIntermediate).toList(); + + // 应用存储限制,保留最新的1000条完成态记录 + List historyToSave = finalRecords; + if (finalRecords.length > _maxStorageItems) { + historyToSave = + finalRecords.sublist(finalRecords.length - _maxStorageItems); + } + + // 保存到本地存储 final List> historyJson = - translationHistory.map((item) => item.toJson()).toList(); + historyToSave.map((item) => item.toJson()).toList(); _storage.write(_historyKey, jsonEncode(historyJson)); + + Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录'); } catch (e) { Logger.error('保存翻译历史失败: ${e.toString()}'); } } + // 获取所有完成态历史记录(供历史页面使用) + // 【修复】简化实现,合并存储和内存中的完成态记录 + List getAllHistory() { + try { + final List result = []; + final Set existingIds = {}; + + // 添加存储的历史记录(存储中的记录都是完成态) + final historyJson = _storage.read(_historyKey); + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + final List 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() { translationHistory.clear(); _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 historyList = jsonDecode(historyJson); + final List storedHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + final originalStoredCount = storedHistory.length; + storedHistory.removeWhere((item) => item.sessionId == sessionId); + removedFromStorage = originalStoredCount - storedHistory.length; + + // 重新保存更新后的存储数据 + final List> 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 historyList = jsonDecode(historyJson); + final List 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> 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}) { _scrollToBottom(); } - - /// 处理原生语音交互开始事件 - void _handleVoiceInteractionStarted() { - // 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS - stopAll(); - } } // 翻译项模型 diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index 78f284cee..a33bf50ac 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../data/services/volcano_ai_service.dart'; import '../controllers/translation_controller.dart'; @@ -50,7 +51,7 @@ class TranslationDetailController extends GetxController { // 异步初始化方法 Future _asyncInitialize() async { isLoading.value = true; - + try { // 使用 Future.microtask 将操作放到后台线程 await Future.microtask(() { @@ -116,13 +117,13 @@ class TranslationDetailController extends GetxController { Future extractSummary(String text) async { return _extractSummaryInternal(text, _defaultTimeoutDuration); } - + // 内部摘要提取方法,避免代码重复 Future _extractSummaryInternal(String text, int timeoutDuration) async { if (text.isEmpty) { return '无法生成摘要:文本为空'; } - + isGeneratingSummary.value = true; try { @@ -155,7 +156,7 @@ class TranslationDetailController extends GetxController { messages: messages, systemPrompt: systemPrompt, ).timeout(Duration(seconds: timeoutDuration), onTimeout: () { - throw TimeoutException('请求超时,请稍后重试'); + throw TimeoutException('请求超时', Duration(seconds: timeoutDuration)); }); } on TimeoutException catch (_) { final timeoutMessage = timeoutDuration == _defaultTimeoutDuration @@ -182,10 +183,63 @@ class TranslationDetailController extends GetxController { } } + // 删除单条翻译记录 + Future 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 void onClose() { // 确保资源被正确释放 historyController.translationController.stopAll(); super.onClose(); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_history_controller.dart b/lib/modules/translation/controllers/translation_history_controller.dart index f5679c286..3c2184c43 100644 --- a/lib/modules/translation/controllers/translation_history_controller.dart +++ b/lib/modules/translation/controllers/translation_history_controller.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import '../controllers/translation_controller.dart'; @@ -44,9 +45,12 @@ class TranslationHistoryController extends GetxController { // 按日期对历史记录进行分组 void _groupHistoryByDate() { final Map> newGroupedHistory = {}; - + + // 获取完整的历史记录,而不仅仅是当前显示的记录 + final List allHistory = translationController.getAllHistory(); + // 筛选出每个会话的第一条记录 - final List firstItemsOfSessions = translationController.translationHistory + final List firstItemsOfSessions = allHistory .where((item) => item.isFirstInSession) .toList(); @@ -98,12 +102,15 @@ class TranslationHistoryController extends GetxController { // 按会话ID对历史记录进行分组 void _groupHistoryBySession() { final Map> newSessionMap = {}; - - for (var item in translationController.translationHistory) { + + // 获取完整的历史记录进行会话分组 + final List allHistory = translationController.getAllHistory(); + + for (var item in allHistory) { if (!newSessionMap.containsKey(item.sessionId)) { newSessionMap[item.sessionId] = []; } - + newSessionMap[item.sessionId]!.add(item); } @@ -133,4 +140,35 @@ class TranslationHistoryController extends GetxController { // 检查是否有历史记录 bool get hasHistory => translationController.translationHistory.isNotEmpty; -} \ No newline at end of file + + // 删除指定会话的历史记录 + Future 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), + ); + } + } +} \ No newline at end of file diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index 9e94ecbdf..fe0105f51 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -72,14 +72,14 @@ class TranslationDetailView extends GetView { ), onPressed: () => Get.back(), ), - title: Text( - 'Detail', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 24.sp, - fontWeight: FontWeight.bold, - ), - ), + // title: Text( + // 'Detail', + // style: TextStyle( + // color: isDarkMode ? Colors.white : Colors.black, + // fontSize: 24.sp, + // fontWeight: FontWeight.bold, + // ), + // ), centerTitle: true, actions: [ IconButton( @@ -102,21 +102,22 @@ class TranslationDetailView extends GetView { children: [ Expanded( child: Obx(() => _buildTabButton( - title: '原始记录', + title: 'originalRecord'.tr, // 原始记录 isSelected: controller.selectedTabIndex.value == 0, onTap: () => controller.switchTab(0), isDarkMode: isDarkMode, )), ), - SizedBox(width: 12.w), - Expanded( - child: Obx(() => _buildTabButton( - title: '记录摘要', - isSelected: controller.selectedTabIndex.value == 1, - onTap: () => controller.switchTab(1), - isDarkMode: isDarkMode, - )), - ), + //屏蔽“记录摘要”,需要是开启 + // SizedBox(width: 12.w), + // Expanded( + // child: Obx(() => _buildTabButton( + // title: 'recordSummary'.tr, // 记录摘要 + // isSelected: controller.selectedTabIndex.value == 1, + // onTap: () => controller.switchTab(1), + // isDarkMode: isDarkMode, + // )), + // ), ], ), ); @@ -210,85 +211,105 @@ class TranslationDetailView extends GetView { // 构建会话详情中的翻译项 Widget _buildSessionItemWidget(TranslationItem item, bool isDarkMode) { + final context = Get.context!; return Container( - width: double.infinity, - margin: EdgeInsets.only(bottom: 16.h), + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(isDarkMode ? 30 : 8), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], + color: item.isIntermediate + ? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色 + : (item.sourceLanguageCode == controller.currentItem.sourceLanguageCode + ? Theme.of(context) + .colorScheme + .primary + .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( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 源文本部分 - Padding( - padding: EdgeInsets.fromLTRB(20.r, 20.r, 20.r, 10.r), - child: Text( - item.sourceText, + // 源文本 + Text( + item.sourceText, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + if (item.translatedText.isNotEmpty) ...[ + const SizedBox(height: 8), + // 翻译文本 + Text( + item.translatedText, style: TextStyle( - fontSize: 15.sp, - color: isDarkMode ? Colors.white : Colors.black, + fontSize: 16, + color: Theme.of(context).textTheme.bodyMedium?.color, fontWeight: FontWeight.w500, ), ), - ), - // 翻译文本部分 - Padding( - padding: EdgeInsets.fromLTRB(20.r, 10.r, 20.r, 16.r), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Text( - item.translatedText, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[700], - fontWeight: FontWeight.normal, - ), - ), + ], + // 语言信息和操作按钮 + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color, ), - 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) { @@ -340,7 +361,7 @@ class TranslationDetailView extends GetView { padding: EdgeInsets.all(24.r), child: Center( child: Text( - '点击下方按钮生成会话摘要', + 'clickToGenerateSummary'.tr, // 点击下方按钮生成会话摘要 style: TextStyle( fontSize: 15.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -363,7 +384,7 @@ class TranslationDetailView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '摘要内容', + 'summaryContent'.tr, // 摘要内容 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, @@ -438,8 +459,8 @@ class TranslationDetailView extends GetView { void _copySummaryToClipboard() { Clipboard.setData(ClipboardData(text: controller.summaryResult.value)); Get.snackbar( - '已复制', - '摘要已复制到剪贴板', + 'copied'.tr, // 已复制 + 'summaryCopiledToClipboard'.tr, // 摘要已复制到剪贴板 snackPosition: SnackPosition.BOTTOM, duration: Duration(seconds: 2), ); @@ -469,7 +490,7 @@ class TranslationDetailView extends GetView { child: controller.isGeneratingSummary.value ? _buildGeneratingIndicator(isDarkMode) : Text( - controller.summaryResult.value.isEmpty ? '提取摘要' : '已生成摘要', + controller.summaryResult.value.isEmpty ? 'extractSummary'.tr : 'summaryGenerated'.tr, // 提取摘要 : 已生成摘要 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -494,7 +515,7 @@ class TranslationDetailView extends GetView { ), SizedBox(width: 12.w), Text( - '生成中...', + 'generating'.tr, // 生成中... style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -524,7 +545,7 @@ class TranslationDetailView extends GetView { ), ), child: Text( - '重新生成', + 'regenerate'.tr, // 重新生成 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -549,14 +570,14 @@ class TranslationDetailView extends GetView { Get.dialog( AlertDialog( title: Text( - '请求超时', + 'requestTimeout'.tr, // 请求超时 style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, ), ), content: Text( - '生成摘要请求超时,可能是网络问题或服务器繁忙。', + 'summaryTimeoutMessage'.tr, // 生成摘要请求超时,可能是网络问题或服务器繁忙。 style: TextStyle( fontSize: 16.sp, ), @@ -565,7 +586,7 @@ class TranslationDetailView extends GetView { TextButton( onPressed: () => Get.back(), child: Text( - '取消', + 'cancel'.tr, // 取消 style: TextStyle( fontSize: 16.sp, color: Colors.grey[700], @@ -579,7 +600,7 @@ class TranslationDetailView extends GetView { controller.retryExtractSummaryWithExtendedTimeout(text); }, child: Text( - '重试', + 'retry'.tr, // 重试 style: TextStyle( fontSize: 16.sp, color: Colors.blue, @@ -595,23 +616,119 @@ class TranslationDetailView extends GetView { ); } + // 显示删除单条记录确认对话框 + 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() { String shareText = ''; - + if (controller.selectedTabIndex.value == 0) { // 分享原始记录 final String sourceText = _getCombinedSourceText(); final String translatedText = _getCombinedTranslatedText(); - shareText = '原文:\n$sourceText\n\n译文:\n$translatedText'; + shareText = '${'originalText'.tr}:\n$sourceText\n\n${'translatedText'.tr}:\n$translatedText'; // 原文:译文: } else { // 分享摘要 if (controller.summaryResult.value.isNotEmpty) { - shareText = '会话摘要:\n${controller.summaryResult.value}'; + shareText = '${'sessionSummary'.tr}:\n${controller.summaryResult.value}'; // 会话摘要: } else { Get.snackbar( - '无法分享', - '请先生成摘要', + 'cannotShare'.tr, // 无法分享 + 'generateSummaryFirst'.tr, // 请先生成摘要 snackPosition: SnackPosition.BOTTOM, duration: Duration(seconds: 2), ); @@ -622,7 +739,7 @@ class TranslationDetailView extends GetView { // 调用系统分享 Share.share( 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)}', // 翻译会话记录 ); } } \ No newline at end of file diff --git a/lib/modules/translation/views/translation_history_view.dart b/lib/modules/translation/views/translation_history_view.dart index 1283a4c27..52b35faca 100644 --- a/lib/modules/translation/views/translation_history_view.dart +++ b/lib/modules/translation/views/translation_history_view.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:intl/intl.dart'; import '../controllers/translation_controller.dart'; import '../controllers/translation_history_controller.dart'; import '../bindings/translation_detail_binding.dart'; import 'translation_detail_view.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../data/services/language_manager.dart'; class TranslationHistoryView extends GetView { const TranslationHistoryView({Key? key}) : super(key: key); @@ -116,7 +118,7 @@ class TranslationHistoryView extends GetView { child: Padding( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( @@ -144,9 +146,52 @@ class TranslationHistoryView extends GetView { maxLines: 2, 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( Icons.chevron_right, color: isDarkMode ? Colors.grey[600] : Colors.grey[350], @@ -161,7 +206,7 @@ class TranslationHistoryView extends GetView { void _showTranslationDetail(TranslationItem item) { // 获取该会话的所有翻译项 final List sessionItems = controller.getSessionItems(item.sessionId); - + Get.to( () => TranslationDetailView(), arguments: { @@ -173,9 +218,92 @@ class TranslationHistoryView extends GetView { ); } + // 格式化会话时间 + 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(); + + // 获取源语言和目标语言的中文名称 + 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() { final isDarkMode = Get.isDarkMode; - + Get.dialog( AlertDialog( backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, diff --git a/lib/modules/translation/views/translation_list_view.dart b/lib/modules/translation/views/translation_list_view.dart index dd13b7521..d559a8d8e 100644 --- a/lib/modules/translation/views/translation_list_view.dart +++ b/lib/modules/translation/views/translation_list_view.dart @@ -37,7 +37,7 @@ class TranslationListView extends GetView { onPressed: () => Get.back(), ), title: Text( - "翻译录音文件", // + "translateAudioFiles".tr, // 翻译录音文件 style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w600, @@ -52,7 +52,7 @@ class TranslationListView extends GetView { return Obx(() { final fileList = controller.files; // 不要 toList() if (fileList.isEmpty) { - return Center(child: Text('暂无音频文件')); + return Center(child: Text('noAudioFiles'.tr)); // 暂无音频文件 } return ListView.builder( @@ -75,17 +75,17 @@ class TranslationListView extends GetView { return FutureBuilder( future: controller.getAudioDuration(file.path), builder: (context, snapshot) { - String durationText = "计算中..."; + String durationText = "calculating".tr; // 计算中... if (snapshot.hasData) { final duration = snapshot.data!; if (duration.inSeconds > 0) { durationText = "${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}"; } else { - durationText = "未知"; + durationText = "unknown".tr; // 未知 } } else if (snapshot.hasError) { - durationText = "错误"; + durationText = "error".tr; // 错误 } return GestureDetector( onLongPressStart: (details) { @@ -216,8 +216,8 @@ class TranslationListView extends GetView { // 播放状态文本 Text( isCurrentFile - ? (isPlaying ? '播放中...' : '已暂停') - : '点击播放', + ? (isPlaying ? 'playing'.tr : 'paused'.tr) // 播放中... : 已暂停 + : 'clickToPlay'.tr, // 点击播放 style: TextStyle( color: Colors.blue, fontSize: 10.sp, @@ -269,7 +269,7 @@ class TranslationListView extends GetView { size: 20.w, color: isDarkMode ? Colors.white : Colors.black87, ), - '重命名', + 'rename'.tr, // 重命名 isDarkMode ? Colors.white : Colors.black87, () { // 弹出重命名输入框 @@ -282,7 +282,7 @@ class TranslationListView extends GetView { size: 20.w, color: isDarkMode ? Colors.white : Colors.black87, ), - '智能转写', + 'smartTranscription'.tr, // 智能转写 isDarkMode ? Colors.white : Colors.black87, () { Get.toNamed(Routes.meeting, arguments: { @@ -296,7 +296,7 @@ class TranslationListView extends GetView { size: 20.w, color: isDarkMode ? Colors.red[700] : Colors.red, ), - '删除', + 'delete'.tr, // 删除 isDarkMode ? Colors.red[700]! : Colors.red, () { file.delete(); @@ -358,7 +358,7 @@ class TranslationListView extends GetView { mainAxisSize: MainAxisSize.min, children: [ Text( - '重命名音频文件', + 'renameAudioFile'.tr, // 重命名音频文件 style: TextStyle( color: isDarkMode ? Colors.white : Colors.black87, fontSize: 16.sp, @@ -390,7 +390,7 @@ class TranslationListView extends GetView { fontSize: 16.sp, ), decoration: InputDecoration( - hintText: '请输入新的文件名', + hintText: 'enterNewFileName'.tr, // 请输入新的文件名 hintStyle: TextStyle( color: isDarkMode ? Colors.white70 : Colors.black54, fontSize: 16.sp, @@ -422,7 +422,7 @@ class TranslationListView extends GetView { borderRadius: BorderRadius.circular(12.w), ), child: Text( - '取消', + 'cancel'.tr, // 取消 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? AppColors.primary : Colors.blue, @@ -455,7 +455,7 @@ class TranslationListView extends GetView { borderRadius: BorderRadius.circular(12.w), ), child: Text( - '确认', + 'confirm'.tr, // 确认 style: TextStyle( color: Colors.white, fontSize: 14.sp, diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index e529bd98d..1cd6779e3 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -154,7 +154,7 @@ class TranslationView extends GetView { size: 24.sp, ), onPressed: controller.toggleisRecord, - tooltip: '是否开启录音', + tooltip: 'enableRecording'.tr, // 是否开启录音 ), IconButton( icon: Icon( @@ -167,7 +167,20 @@ class TranslationView extends GetView { Get.toNamed(Routes.translationList); 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 { speakerId: 1, activeSpeaker: controller.activeSpeaker.value, icon: Icons.headset, // 耳机图标 - label: "耳机用户", + label: "headsetUser".tr, // 耳机用户 ), SizedBox(width: 40.w), // 手机图标按钮 (说话者2) @@ -424,7 +437,7 @@ class TranslationView extends GetView { speakerId: 2, activeSpeaker: controller.activeSpeaker.value, icon: Icons.phone_android, // 手机图标 - label: "手机用户", + label: "phoneUser".tr, // 手机用户 ), ], ); diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 132b8efa1..01b42c684 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -352,7 +352,6 @@ object AgentService : CoroutineScope { */ private fun startIdleCheck() { stopIdleCheck() // 先停止现有的检查 - Log.d(TAG, "启动空闲检测: ${isRecognitionActive}") if (!isRecognitionActive) return idleCheckJob = launch { @@ -360,7 +359,6 @@ object AgentService : CoroutineScope { delay(maxIdleSeconds * 1000L) if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { - Log.d(TAG, "启动空闲检测: stopRecognition") //stopRecognition() sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds)) } @@ -398,14 +396,12 @@ object AgentService : CoroutineScope { Log.d(TAG, "服务未初始化") return false } - Log.d(TAG, "麦克风音频未暂停,无法设置${azureAsrHelper!!.isPaused},$isExternalActive") if(!(azureAsrHelper!!.isPaused)&&isExternalActive) { Log.d(TAG, "麦克风音频未暂停,无法设置") BleService.closeCodec() return false } - Log.d(TAG, "开始语音识别") if (isRecognitionActive) return true // 检查录音权限 @@ -499,14 +495,13 @@ object AgentService : CoroutineScope { _isRecognitionActive.set(false) stopIdleCheck() - Log.d(TAG, "onSessionStopped") audioPlayer?.playAudio(R.raw.stop) } override fun onCanceled(reason: String, errorDetails: String) { _isRecognitionActive.set(false) stopIdleCheck() - Log.d(TAG, "recognition_canceled") + BleService.closeCodec() sendEvent("recognition_canceled", mapOf( "reason" to reason, @@ -517,7 +512,7 @@ object AgentService : CoroutineScope { override fun onError(error: String) { _isRecognitionActive.set(false) stopIdleCheck() - Log.d(TAG, "error") + BleService.closeCodec() Log.e(TAG, "语音识别错误: $error") sendEvent("error", mapOf( @@ -549,7 +544,6 @@ object AgentService : CoroutineScope { try { azureAsrHelper?.stopContinuousRecognition() - Log.d(TAG, "停止语音识别") BleService.closeCodec() _isRecognitionActive.set(false) stopIdleCheck() @@ -569,7 +563,7 @@ object AgentService : CoroutineScope { try { if(audioSourceType== AzureAsrHelper.AudioSourceType.EXTERNAL) { - Log.d(TAG, "当前是什么输入:$audioSourceType") + BleService.closeCodec() stopRecognition() startRecognition(false) @@ -1447,12 +1441,12 @@ object AgentService : CoroutineScope { // Log.d(TAG, "mcp调用结果: $metaStr") if (meta.has("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 name = cardMusic.optString("name", "") val sgener = cardMusic.optString("sgener", "") val image = cardMusic.optString("image", "") - val ts = cardMusic.optInt("ts", 0) + val ts = cardMusic.optLong("ts", 0) processMusicPlay( mapOf( "id" to id, @@ -1470,12 +1464,12 @@ object AgentService : CoroutineScope { val playlist = mutableListOf>() for (i in 0 until musics.length()) { val item = musics.optJSONObject(i) ?: continue - val id = item.optInt("id", 0) + val id = item.optLong("id", 0) val url = item.optString("url", "") val name = item.optString("name", "") val sgener = item.optString("sgener", "") val image = item.optString("image", "") - val ts = item.optInt("ts", 0) + val ts = item.optLong("ts", 0) playlist.add( mapOf( "id" to id, diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index aaa4887c5..661e51ec8 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/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 { _, _ -> // 直接在当前线程调用回调 Log.d(tag, "会话结束事件") + if (audioSourceType == AudioSourceType.EXTERNAL) { + callback.onSessionStopped() isContinuousRecognitionActive = false stopAudioProcessing() + } } ) @@ -633,7 +636,12 @@ class AzureAsrHelper(private val context: Context) { Log.d(tag, "关闭麦克风stopAudioPause: ") // 清理旧的识别器 isPaused = true - + // 如果正在进行连续识别,先停止 + if (isContinuousRecognitionActive) { + // 直接停止,不等待结果 + recognizer?.stopContinuousRecognitionAsync() + isContinuousRecognitionActive = false + } } @@ -642,9 +650,15 @@ class AzureAsrHelper(private val context: Context) { */ fun audioContinue() { Log.d(tag, "继续麦克风stopAudioPause: ") - +// 如果正在进行连续识别,先停止 + if (!isContinuousRecognitionActive) { + // 直接停止,不等待结果 + recognizer?.startContinuousRecognitionAsync() + isContinuousRecognitionActive = true + } // 开始连续识别 isPaused = false + } /** diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 2e11c4301..be1cabf4d 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/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 val isRunning = AtomicBoolean(false) - private var currentIdInternal: String? = null + private var currentIdInternal: Long? = null private var currentStatus = false private var playlist = mutableListOf>() private var currentIndex = 0 @@ -48,7 +48,7 @@ class MusicService : MediaBrowserServiceCompat() { fun isServiceRunning() = isRunning.get() fun isPlaying() = currentStatus - fun getCurrentId(): String? = currentIdInternal + fun getCurrentId(): Long? = currentIdInternal // // 获取播放进度信息 // fun getProgress(): Map { @@ -118,11 +118,11 @@ class MusicService : MediaBrowserServiceCompat() { fun updateMediaSessionMetadata(song: Map) { instance?.let { service -> try { - val title = song["title"] as? String ?: song["name"] as? String ?: "未知标题" - val artist = song["artist"] as? String ?: song["sgener"] as? String ?: "未知歌手" - val coverUrl = song["coverUrl"] as? String ?: song["image"] as? String ?: "" -currentIdInternal = song["id"] as? String ?: song["id"] as? String ?: "0" - Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal") + val title = song["title"] as? String ??: "未知标题" + val artist = song["artist"] as? String ??: "未知歌手" + val coverUrl = song["coverUrl"] as? String ??: "" + currentIdInternal = (song["id"] as? Number)?.toLong() ?: 0L + Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal") 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_ARTIST, artist) @@ -216,13 +216,12 @@ private fun updateProgress() { // 更新媒体会话进度 // 通知Flutter层 - flutterChannel?.invokeMethod("onMediaSessionSeekTo1", - mapOf( + flutterChannel?.invokeMethod("onMediaSessionProgress", mapOf( "currentIdInternal" to currentIdInternal, - "currentPosition" to currentPosition, - "duration" to duration, - "progress" to progress - )) + "currentPosition" to currentPosition, + "duration" to duration, + "progress" to progress + )) } } } @@ -238,19 +237,20 @@ private fun updateProgress() { when (command) { "play" -> intent.getSerializableExtra("song")?.let { url -> val songMap = intent.getSerializableExtra("song") as? Map + Log.i(TAG, "MusicePlay onStartCommand: $songMap") songMap?.let { song -> - val songId = song["id"]as? String ?:"0" - Log.i(TAG, "command songId: $songId") + val songId = (song["id"] as? Number)?.toLong() ?: 0L + currentIdInternal = songId - val existingIndex = playlist.indexOfFirst { it["id"] == songId } + val existingIndex = playlist.indexOfFirst { it["id"] == songId} if (existingIndex == -1) { playlist.add(song) currentIndex = playlist.size - 1 } else { currentIndex = existingIndex } - + Log.i(TAG, "MusicePlay songId: $songId currentIndex:$currentIndex" ) play(song) } } @@ -546,8 +546,8 @@ private fun updateProgress() { private fun play(song: Map) { val url = song["url"] as? String ?: return Log.i(TAG, "开始播放: $url") - val id = song["id"] as? String ?: "0" - Log.i(TAG, "开始播放play id: $id") + val id = (song["id"] as? Number)?.toLong() ?: 0L + Log.i(TAG, "开始播放play id: $id") val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String @@ -586,8 +586,8 @@ private fun updateProgress() { val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String - val id = song["id"] as? String ?: "0" - Log.i(TAG, "pause id: $id") + val id =(song["id"] as? Number)?.toLong() ?: 0L + Log.i(TAG, "pause id: $id") currentIdInternal = id updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PAUSED); @@ -607,8 +607,8 @@ private fun updateProgress() { val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String - val id = song["id"] as? String ?: "0" - Log.i(TAG, "resume id: $id") + val id = (song["id"] as? Number)?.toLong() ?: 0L + Log.i(TAG, "resume id: $id") currentIdInternal = id updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PLAYING); diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index 8be33fe68..b7007500c 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -1,3 +1,5 @@ +import 'dart:ffi'; + import 'package:flutter/services.dart'; class MusicService { @@ -101,7 +103,7 @@ class MusicService { required Function() onPrevious, required Function(int) onSeekTo, required Function() onStop, - required Function(String, int, int, double) onStop1, + required Function(int, int, int, double) onProgress, }) async { // 设置方法调用处理器来处理来自原生平台的回调 _methodChannel.setMethodCallHandler((call) async { @@ -125,13 +127,12 @@ class MusicService { case 'onMediaSessionStop': onStop(); break; - case 'onMediaSessionSeekTo1': - final currentIdInternal = - call.arguments['currentIdInternal'] as String; + case 'onMediaSessionProgress': + final currentIdInternal = call.arguments['currentIdInternal'] as int; final currentPosition = call.arguments['currentPosition'] as int; final duration = call.arguments['duration'] as int; final progress = call.arguments['progress'] as double; - onStop1(currentIdInternal, currentPosition, duration, progress); + onProgress(currentIdInternal, currentPosition, duration, progress); break; } }); diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt index 8f68906de..c030cb725 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt +++ b/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 isDisconnect = false if(otaManager!= null) { - - otaManager?.bluetoothHelper?.stopScan() - - otaManager?.bluetoothHelper?.disconnectDevice(otaDevice) + BleService.removeCallback(this) + otaManager?.release() + otaManager= null + bluetoothHelper=null } result.success("") } else {