Browse Source

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

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

3
lib/core/bindings/initial_binding.dart

@ -4,6 +4,7 @@ import 'package:deep_voice/data/services/navigation_manager.dart';
import 'package:get/get.dart';
import '../../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>(() => NavigationManager(), fenix: true);
Get.lazyPut<LocationManager>(() => LocationManager(), fenix: true);
// 火山AI服务
// Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 账号管理服务
Get.lazyPut<AccountService>(() => AccountService(), fenix: true);

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

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

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

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

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

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

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

@ -364,11 +364,85 @@ const Map<String, String> zhCN = {
"healthAdvisorWelcome": "您好,我是吴小江,很高兴能为您提供多功能AI聊天。我将用专业知识帮助您。请问有什么问题想咨询吗?",
"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": "用户反馈",

38
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]}');

4
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');

318
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<void> loadMoreMessages() async {
// 防止重复加载
if (isLoadingMore.value || !hasMoreMessages.value) {
Logger.d(TAG, '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreMessages=${hasMoreMessages.value}');
return;
}
try {
isLoadingMore.value = true;
isLoadingMoreError.value = false;
final nextPage = currentPage.value + 1;
Logger.i(TAG, '正在加载第 $nextPage 页历史消息...');
final result = await ChatStorage.getMessages(
sessionId: sessionId,
page: nextPage,
pageSize: pageSize,
);
if (result.messages.isNotEmpty) {
// 转换新消息
final newMessages = _convertChatMessagesToMessages(result.messages);
// 按时间排序新消息
newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
// 添加短暂延迟,让用户感知到加载过程
await Future.delayed(const Duration(milliseconds: 300));
// 将新消息插入到现有消息列表的开头(因为是历史消息)
final updatedMessages = [...newMessages, ...messages];
messages.assignAll(updatedMessages);
// 更新分页状态
currentPage.value = nextPage;
hasMoreMessages.value = nextPage < result.totalPages;
Logger.i(TAG, '成功加载 ${newMessages.length} 条历史消息,当前页: $nextPage/${result.totalPages}');
} else {
// 没有更多消息
hasMoreMessages.value = false;
Logger.i(TAG, '没有更多历史消息');
}
} catch (e) {
Logger.e(TAG, '加载更多消息失败: $e');
isLoadingMoreError.value = true;
} finally {
isLoadingMore.value = false;
}
}
// 转换ChatMessage到Message的通用方法
List<Message> _convertChatMessagesToMessages(List<ChatMessage> chatMessages) {
return chatMessages.map((msg) {
final meta = msg.metadata != null && msg.metadata != ""
? jsonDecode(msg.metadata!)
: null; // 如果为空,使用空对象
// 从元数据中提取图片信息
final hasImage = meta != null && meta.containsKey('hasImage')
? meta['hasImage'] as bool
: false;
final imagePath = meta != null && meta.containsKey('imagePath')
? meta['imagePath'] as String?
: null;
return Message(
isUser: msg.sender == 'user',
text: msg.message,
meta: meta,
timestamp: DateTime.parse(msg.timestamp),
hasImage: hasImage,
imagePath: imagePath,
);
}).toList();
}
// 打印消息详情的通用方法
void _logMessageDetails(List<Message> messageList) {
Logger.d(TAG, '===== 历史消息详情开始 =====');
for (int i = 0; i < messageList.length; i++) {
final msg = messageList[i];
Logger.d(
TAG,
'消息[$i]: {'
'isUser: ${msg.isUser}, '
'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", '
'time: ${msg.timestamp}, '
'hasImage: ${msg.hasImage}, '
'imagePath: ${msg.imagePath ?? "无"}, '
'meta: ${msg.meta != null ? "有" : "无"}'
'}');
}
Logger.d(TAG, '===== 历史消息详情结束 =====');
}
// 设置滚动监听器
void _setupScrollListener() {
scrollController.addListener(() {
// 检查是否滚动到顶部(因为使用了reverse: true,顶部实际是最大滚动位置)
if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) {
// 当距离顶部100像素时触发加载更多
if (hasMoreMessages.value && !isLoadingMore.value) {
Logger.d(TAG, '触发加载更多历史消息');
loadMoreMessages();
}
}
});
}
// 手动触发加载更多(用于测试或手动调用)
void triggerLoadMore() {
if (hasMoreMessages.value && !isLoadingMore.value) {
Logger.i(TAG, '手动触发加载更多历史消息');
loadMoreMessages();
} else {
Logger.w(TAG, '无法加载更多:hasMoreMessages=${hasMoreMessages.value}, isLoadingMore=${isLoadingMore.value}');
}
}
// 重置分页状态(用于刷新聊天记录)
void resetPagination() {
currentPage.value = 1;
hasMoreMessages.value = true;
isLoadingMore.value = false;
isLoadingMoreError.value = false;
Logger.i(TAG, '分页状态已重置');
}
// 添加初始欢迎消息
void _addWelcomeMessage() {
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 = <String, String>{
'id': song['id']?.toString() ?? '',
final musicData = <String, dynamic>{
'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<void> 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<void> updateMusicProgress1(String currentIdInternal, int newPosition,
Future<void> 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<void> seekToPosition(int positionMs) async {
try {
await _musiceManager.seekTo(positionMs);
// 立即更新进度状态
// await updateMusicProgress();
} catch (e) {
Logger.e('AgentController', '跳转播放位置失败: $e');
}
}
// 跳转到指定位置
Future<void> seekToPosition1(String currentIdInternal, int newPosition,
Future<void> 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<void> onMediaSessionSeekTo1(String currentIdInternal, int newPosition,
/// 处理来自系统进度条
Future<void> 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);
}
}

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

@ -200,8 +200,13 @@ class _AgentViewState extends State<AgentView> 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<AgentView> with WidgetsBindingObserver {
),
);
}
// 构建加载更多指示器
Widget _buildLoadMoreIndicator() {
return Obx(() {
if (controller.isLoadingMore.value) {
// 正在加载状态
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: const Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 8),
Text(
'加载更多消息...',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
);
} else if (controller.isLoadingMoreError.value) {
// 加载失败状态
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: GestureDetector(
onTap: () => controller.loadMoreMessages(),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.refresh,
size: 16,
color: Colors.grey,
),
SizedBox(width: 8),
Text(
'加载失败,点击重试',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
),
);
} else {
// 默认状态(有更多消息但未加载)
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: const Center(
child: Text(
'向上滑动查看更多',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
),
);
}
});
}
}

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

@ -200,7 +200,7 @@ Widget buildMusicContent(
...playlist.asMap().entries.take(4).map((entry) {
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"] ?? "未知歌手";

4
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<AgentController>();
final shouldShowAnimation =
controller.isListening.value || controller.isPushToTalkActive.value;
return SoundBarsWidget(
isActive: controller.isListening.value,
isActive: shouldShowAnimation,
barCount: barCount,
);
});

15
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<String, dynamic> song,
List<Map<String, dynamic>> playlist) {
final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0;
final songId = song['id'] ?? 0;
controller.updateCurrentPlaylistContext(playlist, songId);
controller.musicPlay(song);
}

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

@ -89,33 +89,17 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
),
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<OtaUpgradeController> {
);
}
// 设备信息卡片
Widget _buildDeviceCard(bool isDarkMode) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [
const Color(0xFF1A1A1A).withValues(alpha: 0.9),
const Color(0xFF2A2A2A).withValues(alpha: 0.9),
]
: [
const Color(0xFFF8FAFF),
const Color(0xFFE3F2FD),
],
),
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.08),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 0,
),
],
),
child: Padding(
padding: EdgeInsets.all(24.w),
child: Column(
children: [
// 设备图标区域
_buildDeviceIcon(isDarkMode),
SizedBox(height: 24.h),
// 版本信息区域
_buildVersionInfo(isDarkMode),
],
),
),
);
}
// 设备图标
Widget _buildDeviceIcon(bool isDarkMode) => Image.asset(
Widget _buildDeviceIcon(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.05)
: Colors.white.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: isDarkMode
? Colors.white.withValues(alpha: 0.1)
: Colors.blue.withValues(alpha: 0.1),
width: 1,
),
),
child: Image.asset(
isDarkMode
? '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<Color>(
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<Color>(
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],
),
),
],
),
],
),
);

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

@ -305,44 +305,44 @@ class PairingView extends GetView<PairingController> {
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<PairingController> {
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<PairingController> {
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<PairingController> {
elevation: 0,
),
child: Text(
'授权绑定',
'authorizeBind'.tr, // 授权绑定
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -596,7 +596,7 @@ class PairingView extends GetView<PairingController> {
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<PairingController> {
minimumSize: Size(double.infinity, 48.h),
),
child: Text(
'重新扫描'.tr, // 稍后再说
'rescan'.tr, // 重新扫描
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,

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

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

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

@ -841,7 +841,7 @@ class SettingsView extends GetView<SettingsController> {
//------------测试使用标记 start---------------//
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),

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

@ -10,6 +10,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart';
import 'package: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<BleManager>();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
// 音频输入源
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<void> suspendOtherServices() async {
try {
var status = await _musiceManager.getStatus();
var isPlaying = status['isPlaying'] ?? false;
if (isPlaying == true) {
isContinuePlaying = true;
await _musiceManager.pause(); // 暂停音乐
} else {
isContinuePlaying = false;
}
} catch (e) {
Logger.error('初始化服务失败: ${e.toString()}');
}
}
// 恢复其他服务
Future<void> restoreOtherServices() async {
try {
if (isContinuePlaying) await _musiceManager.resume(); // 暂停音乐
} catch (e) {
Logger.error('初始化服务失败: ${e.toString()}');
}
}
// 初始化服务
Future<void> _initServices() async {
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<TranslationItem> 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<TranslationItem> finalRecords =
translationHistory.where((item) => !item.isIntermediate).toList();
// 应用存储限制,保留最新的1000条完成态记录
List<TranslationItem> historyToSave = finalRecords;
if (finalRecords.length > _maxStorageItems) {
historyToSave =
finalRecords.sublist(finalRecords.length - _maxStorageItems);
}
// 保存到本地存储
final List<Map<String, dynamic>> historyJson =
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<TranslationItem> getAllHistory() {
try {
final List<TranslationItem> result = <TranslationItem>[];
final Set<String> existingIds = <String>{};
// 添加存储的历史记录(存储中的记录都是完成态)
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
for (final item in storedHistory) {
final id =
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}';
if (!existingIds.contains(id)) {
existingIds.add(id);
result.add(item);
}
}
}
// 添加当前内存中的完成态记录(排除中间态记录)
for (final item in translationHistory) {
if (!item.isIntermediate) {
final id =
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}';
if (!existingIds.contains(id)) {
existingIds.add(id);
result.add(item);
}
}
}
// 按时间排序
result.sort((a, b) => a.timestamp.compareTo(b.timestamp));
return result;
} catch (e) {
Logger.error('获取完整历史记录失败: ${e.toString()}');
// 发生错误时,返回当前内存中的完成态记录
return translationHistory.where((item) => !item.isIntermediate).toList();
}
}
// 清除翻译历史
void clearTranslationHistory() {
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<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
final originalStoredCount = storedHistory.length;
storedHistory.removeWhere((item) => item.sessionId == sessionId);
removedFromStorage = originalStoredCount - storedHistory.length;
// 重新保存更新后的存储数据
final List<Map<String, dynamic>> updatedHistoryJson =
storedHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(updatedHistoryJson));
}
// 3. 刷新UI显示
translationHistory.refresh();
Logger.info(
'删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条');
} catch (e) {
Logger.error('删除会话历史失败: ${e.toString()}');
rethrow; // 重新抛出异常,让调用方处理
}
}
// 删除指定的单条翻译记录
void deleteTranslationItem(String sessionId, DateTime timestamp) {
try {
// 1. 从内存中删除指定的翻译记录
final originalCount = translationHistory.length;
translationHistory.removeWhere((item) =>
item.sessionId == sessionId &&
item.timestamp.millisecondsSinceEpoch ==
timestamp.millisecondsSinceEpoch);
final removedFromMemory = originalCount - translationHistory.length;
// 2. 从本地存储中删除指定的翻译记录
final historyJson = _storage.read(_historyKey);
int removedFromStorage = 0;
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
final originalStoredCount = storedHistory.length;
storedHistory.removeWhere((item) =>
item.sessionId == sessionId &&
item.timestamp.millisecondsSinceEpoch ==
timestamp.millisecondsSinceEpoch);
removedFromStorage = originalStoredCount - storedHistory.length;
// 重新保存更新后的存储数据
final List<Map<String, dynamic>> updatedHistoryJson =
storedHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(updatedHistoryJson));
}
// 3. 刷新UI显示
translationHistory.refresh();
Logger.info(
'删除单条翻译记录成功: sessionId=$sessionId, timestamp=${timestamp.toIso8601String()}, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条');
} catch (e) {
Logger.error('删除单条翻译记录失败: ${e.toString()}');
rethrow; // 重新抛出异常,让调用方处理
}
}
// 滚动到底部 (公共方法,用于向后兼容)
void scrollToBottom({bool animate = true}) {
_scrollToBottom();
}
/// 处理原生语音交互开始事件
void _handleVoiceInteractionStarted() {
// 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS
stopAll();
}
}
// 翻译项模型

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

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

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

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

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

@ -72,14 +72,14 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
onPressed: () => Get.back(),
),
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<TranslationDetailController> {
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<TranslationDetailController> {
// 构建会话详情中的翻译项
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<TranslationDetailController> {
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<TranslationDetailController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'摘要内容',
'summaryContent'.tr, // 摘要内容
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
@ -438,8 +459,8 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
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<TranslationDetailController> {
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<TranslationDetailController> {
),
SizedBox(width: 12.w),
Text(
'生成中...',
'generating'.tr, // 生成中...
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -524,7 +545,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
),
child: Text(
'重新生成',
'regenerate'.tr, // 重新生成
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -549,14 +570,14 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
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<TranslationDetailController> {
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<TranslationDetailController> {
controller.retryExtractSummaryWithExtendedTimeout(text);
},
child: Text(
'重试',
'retry'.tr, // 重试
style: TextStyle(
fontSize: 16.sp,
color: Colors.blue,
@ -595,23 +616,119 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
);
}
// 显示删除单条记录确认对话框
void _showDeleteItemConfirmDialog(TranslationItem item) {
final isDarkMode = Get.isDarkMode;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'deleteTranslationRecord'.tr, // 删除翻译记录
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'deleteTranslationConfirm'.tr, // 确定要删除这条翻译记录吗?此操作无法撤销。
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
SizedBox(height: 12.h),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.sourceText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (item.translatedText.isNotEmpty) ...[
SizedBox(height: 4.h),
Text(
item.translatedText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.black54,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
// 删除指定的翻译记录
controller.deleteTranslationItem(item);
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
// 分享内容方法
void _shareContent() {
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<TranslationDetailController> {
// 调用系统分享
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)}', // 翻译会话记录
);
}
}

134
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<TranslationHistoryController> {
const TranslationHistoryView({Key? key}) : super(key: key);
@ -116,7 +118,7 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
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<TranslationHistoryController> {
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<TranslationHistoryController> {
void _showTranslationDetail(TranslationItem item) {
// 获取该会话的所有翻译项
final List<TranslationItem> sessionItems = controller.getSessionItems(item.sessionId);
Get.to(
() => TranslationDetailView(),
arguments: {
@ -173,9 +218,92 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
);
}
// 格式化会话时间
String _formatSessionTime(DateTime timestamp) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final itemDate = DateTime(timestamp.year, timestamp.month, timestamp.day);
if (itemDate == today) {
// 今天:只显示时间 HH:mm
return DateFormat('HH:mm').format(timestamp);
} else {
// 其他日期:显示月日和时间 MM-dd HH:mm
return DateFormat('MM-dd HH:mm').format(timestamp);
}
}
// 格式化语种对信息
String _formatLanguagePair(TranslationItem item) {
final languageManager = Get.find<LanguageManager>();
// 获取源语言和目标语言的中文名称
final sourceName = languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ?? 'unknownLanguage'.tr; // 未知语言
final targetName = languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ?? 'unknownLanguage'.tr; // 未知语言
// 使用双向箭头符号表示翻译方向
return '$sourceName ⇄ $targetName';
}
// 显示删除会话确认对话框
void _showDeleteSessionConfirmDialog(String sessionId) {
final isDarkMode = Get.isDarkMode;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'deleteSessionRecord'.tr, // 删除会话记录
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Text(
'deleteSessionConfirm'.tr, // 确定要删除该会话的所有翻译记录吗?此操作无法撤销。
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
// 删除指定会话的历史记录
controller.deleteSessionHistory(sessionId);
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
void _showClearHistoryConfirmDialog() {
final isDarkMode = Get.isDarkMode;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,

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

@ -37,7 +37,7 @@ class TranslationListView extends GetView<TranslationListController> {
onPressed: () => Get.back(),
),
title: Text(
"翻译录音文件", //
"translateAudioFiles".tr, // 翻译录音文件
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
@ -52,7 +52,7 @@ class TranslationListView extends GetView<TranslationListController> {
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<TranslationListController> {
return FutureBuilder<Duration>(
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<TranslationListController> {
// 播放状态文本
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<TranslationListController> {
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
'重命名',
'rename'.tr, // 重命名
isDarkMode ? Colors.white : Colors.black87,
() {
// 弹出重命名输入框
@ -282,7 +282,7 @@ class TranslationListView extends GetView<TranslationListController> {
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<TranslationListController> {
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<TranslationListController> {
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<TranslationListController> {
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<TranslationListController> {
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<TranslationListController> {
borderRadius: BorderRadius.circular(12.w),
),
child: Text(
'确认',
'confirm'.tr, // 确认
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,

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

@ -154,7 +154,7 @@ class TranslationView extends GetView<TranslationController> {
size: 24.sp,
),
onPressed: controller.toggleisRecord,
tooltip: '是否开启录音',
tooltip: 'enableRecording'.tr, // 是否开启录音
),
IconButton(
icon: Icon(
@ -167,7 +167,20 @@ class TranslationView extends GetView<TranslationController> {
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<TranslationController> {
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<TranslationController> {
speakerId: 2,
activeSpeaker: controller.activeSpeaker.value,
icon: Icons.phone_android, // 手机图标
label: "手机用户",
label: "phoneUser".tr, // 手机用户
),
],
);

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

@ -352,7 +352,6 @@ object AgentService : CoroutineScope {
*/
private fun startIdleCheck() {
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<Map<String, Any>>()
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,

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

@ -458,9 +458,12 @@ class AzureAsrHelper(private val context: Context) {
EventHandler<SessionEventArgs> { _, _ ->
// 直接在当前线程调用回调
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
}
/**

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

@ -38,7 +38,7 @@ class MusicService : MediaBrowserServiceCompat() {
private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION"
private val isRunning = AtomicBoolean(false)
private var currentIdInternal: String? = null
private var currentIdInternal: Long? = null
private var currentStatus = false
private var playlist = mutableListOf<Map<String,Any>>()
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<String, Any> {
@ -118,11 +118,11 @@ class MusicService : MediaBrowserServiceCompat() {
fun updateMediaSessionMetadata(song: Map<String, Any>) {
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<String, Any>
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<String,Any>) {
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);

11
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;
}
});

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

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

Loading…
Cancel
Save