From 211fa675324d0f9c609838faf04ef9b08ae9d051 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 2 Jul 2025 20:12:06 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8C=89=E4=BD=8F?= =?UTF-8?q?=E8=AF=B4=E8=AF=9D=E7=9A=84=E8=BE=93=E5=85=A5=E6=8E=A7=E4=BB=B6?= =?UTF-8?q?=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/agent/views/agent_view.dart | 1020 ++++++++++++----------- 1 file changed, 523 insertions(+), 497 deletions(-) diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index cca44635e..c0abce98c 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -190,535 +190,568 @@ class _AgentViewState extends State with WidgetsBindingObserver { children: [ // 聊天内容 Expanded( - child: Obx( - () => Align( - alignment: Alignment.topCenter, - child: ListView.builder( - controller: controller.scrollController, - shrinkWrap: true, - reverse: true, - padding: - const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - itemCount: controller.messages.length, - itemBuilder: (context, index) { - final message = controller - .messages[controller.messages.length - 1 - index]; - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: GestureDetector( - onLongPressStart: (LongPressStartDetails details) { - messageActionController.onLongPressMessage( - context, isDarkMode, details, message); - }, - child: MessageBubble( - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - meta: message.meta, - ), - )); - }, + child: Obx( + () => Align( + alignment: Alignment.topCenter, + child: ListView.builder( + controller: controller.scrollController, + shrinkWrap: true, + reverse: true, + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 16), + itemCount: controller.messages.length, + itemBuilder: (context, index) { + final message = controller + .messages[controller.messages.length - 1 - index]; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: GestureDetector( + onLongPressStart: + (LongPressStartDetails details) { + messageActionController.onLongPressMessage( + context, isDarkMode, details, message); + }, + child: MessageBubble( + isUser: message.isUser, + message: message.text, + timestamp: message.timestamp, + isRecognizing: message.isRecognizing, + hasImage: message.hasImage, + imagePath: message.imagePath, + meta: message.meta, + ), + )); + }, + ), + ), ), ), - ), - ), - // 输入区域 - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: backgroundColor, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.06), - blurRadius: 8, - offset: const Offset(0, -1), - ), - ], - border: Border( - top: BorderSide( - color: Colors.grey.withOpacity(0.15), - width: 1, + // 输入区域 + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: backgroundColor, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 8, + offset: const Offset(0, -1), + ), + ], + border: Border( + top: BorderSide( + color: Colors.grey.withOpacity(0.15), + width: 1, + ), + ), ), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 图片预览区域 - 仅在图片输入模式下显示 - Obx(() { - if (controller.isImageInputActive.value && - controller.selectedImagePath.value != null) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 图片预览和关闭按钮 - Stack( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 图片预览区域 - 仅在图片输入模式下显示 + Obx(() { + if (controller.isImageInputActive.value && + controller.selectedImagePath.value != null) { + return Column( + mainAxisSize: MainAxisSize.min, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.file( - File(controller.selectedImagePath.value!), - height: 150, - width: double.infinity, - fit: BoxFit.cover, - ), - ), - Positioned( - top: 8, - right: 8, - child: GestureDetector( - onTap: () => controller.clearImageInput(), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6), - shape: BoxShape.circle, + // 图片预览和关闭按钮 + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file( + File(controller.selectedImagePath.value!), + height: 150, + width: double.infinity, + fit: BoxFit.cover, ), - child: const Icon( - Icons.close, - color: Colors.white, - size: 16, + ), + Positioned( + top: 8, + right: 8, + child: GestureDetector( + onTap: () => controller.clearImageInput(), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 16, + ), + ), ), ), + ], + ), + + // 预设提示选项 + const SizedBox(height: 12), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildPromptChip( + context: context, + label: '总结图片内容'.tr, + onTap: () => controller.summarizeImage(), + ), + const SizedBox(width: 8), + _buildPromptChip( + context: context, + label: '抽取图像文字'.tr, + onTap: () => + controller.extractTextFromImage(), + ), + const SizedBox(width: 8), + _buildPromptChip( + context: context, + label: '翻译图像文字'.tr, + onTap: () => + controller.translateImageText(), + ), + ], ), ), + const SizedBox(height: 12), ], - ), + ); + } else { + return const SizedBox.shrink(); + } + }), - // 预设提示选项 - const SizedBox(height: 12), - SingleChildScrollView( - scrollDirection: Axis.horizontal, + // 输入控件区域 - 包含输入行和展开的选项 + Column( + children: [ + // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 + IntrinsicHeight( child: Row( + crossAxisAlignment: + CrossAxisAlignment.end, // 所有元素底部对齐 children: [ - _buildPromptChip( - context: context, - label: '总结图片内容'.tr, - onTap: () => controller.summarizeImage(), - ), - const SizedBox(width: 8), - _buildPromptChip( - context: context, - label: '抽取图像文字'.tr, - onTap: () => controller.extractTextFromImage(), - ), - const SizedBox(width: 8), - _buildPromptChip( - context: context, - label: '翻译图像文字'.tr, - onTap: () => controller.translateImageText(), - ), - ], - ), - ), - const SizedBox(height: 12), - ], - ); - } else { - return const SizedBox.shrink(); - } - }), - - // 输入控件区域 - 包含输入行和展开的选项 - Column( - children: [ - // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐 - children: [ - // 键盘/语音切换按钮 - Obx(() { - final isTextMode = controller.isTextInputMode.value; - return GestureDetector( - onTap: () { - controller.toggleInputMode(); - }, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(22), - ), - child: Icon( - isTextMode ? Icons.mic_none : Icons.keyboard, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22, - ), - ), - ); - }), - - const SizedBox(width: 8), - - // 文本输入框或语音波形图 - Expanded( - child: Obx(() { - final isTextMode = - controller.isTextInputMode.value; - - if (isTextMode) { - // 文本输入模式 - 限制最大高度并只向上扩展 - return Container( - constraints: const BoxConstraints( - minHeight: 44, // 最小高度 - maxHeight: 120, // 最大高度限制,防止挤压按钮 - ), - padding: const EdgeInsets.symmetric( - horizontal: 16), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(22), - border: Border.all( + // 键盘/语音切换按钮 + Obx(() { + final isTextMode = + controller.isTextInputMode.value; + return GestureDetector( + onTap: () { + controller.toggleInputMode(); + }, + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( color: isDarkMode - ? Colors.grey[ - 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] - : Colors.grey.withOpacity(0.1), - width: 1.0, + ? Colors.grey[800] + : Colors.grey[200], + borderRadius: BorderRadius.circular(22), + ), + child: Icon( + isTextMode + ? Icons.mic_none + : Icons.keyboard, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 22, ), - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.end, // 对齐到底部,实现向上扩展 - children: [ - Expanded( - child: Obx(() { - // 图片模式下使用更高的输入框 - final isImageActive = controller - .isImageInputActive.value; - - return TextField( - controller: - controller.textController, - maxLines: isImageActive - ? 3 - : 5, // 设置合理的最大行数 - minLines: isImageActive ? 2 : 1, - keyboardType: TextInputType - .multiline, // 启用多行键盘 - textInputAction: TextInputAction - .newline, // 支持回车换行 - onTap: () { - // 当点击输入框时,如果'+'选项已展开,则收缩 - if (controller - .isPlusOptionsExpanded - .value) { - controller - .collapsePlusOptions(); - } - }, - style: TextStyle( - fontSize: 15, - color: isDarkMode - ? Colors.white - : Colors.grey[800], - ), - decoration: InputDecoration( - hintText: isImageActive - ? '添加对图片的问题或描述...' - : '输入消息...', - hintStyle: TextStyle( - color: Colors.grey[500], - fontSize: 15, - ), - filled: false, - border: InputBorder.none, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: isDarkMode - ? Colors.grey[800]! - : Colors.grey[200]!, - width: 1.0, - ), - ), - contentPadding: - EdgeInsets.symmetric( - vertical: isImageActive - ? 12 - : 8), // 调整垂直内边距 - ), - onSubmitted: (_) => - controller.sendMessage(), - ); - }), - ), - ], ), ); - } else { - // 语音输入模式 - 按住说话模式 - return GestureDetector( - onLongPressStart: (_) { - // 开始按住说话 - controller.startPushToTalk(); - }, - onLongPressEnd: (_) { - // 结束按住说话 - controller.endPushToTalk(); - }, - onLongPressCancel: () { - // 取消按住说话 - controller.endPushToTalk(); - }, - child: Obx(() { - final isPushToTalkActive = controller.isPushToTalkActive.value; - final isListening = controller.isListening.value; + }), + + const SizedBox(width: 8), + + // 文本输入框或语音波形图 + Expanded( + child: Obx(() { + final isTextMode = + controller.isTextInputMode.value; + if (isTextMode) { + // 文本输入模式 - 限制最大高度并只向上扩展 return Container( - height: 50, + constraints: const BoxConstraints( + minHeight: 44, // 最小高度 + maxHeight: 120, // 最大高度限制,防止挤压按钮 + ), padding: const EdgeInsets.symmetric( horizontal: 16), decoration: BoxDecoration( - color: isPushToTalkActive - ? (isDarkMode ? Colors.blue[900] : Colors.blue[100]) - : (isDarkMode ? Colors.grey[800] : Colors.grey[200]), + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(22), border: Border.all( - color: isPushToTalkActive - ? Colors.blue - : (isDarkMode - ? Colors.grey[800]! - : Colors.grey.withOpacity(0.1)), - width: isPushToTalkActive ? 2.0 : 1.0, + color: isDarkMode + ? Colors.grey[ + 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] + : Colors.grey.withOpacity(0.1), + width: 1.0, ), ), child: Row( + crossAxisAlignment: CrossAxisAlignment + .end, // 对齐到底部,实现向上扩展 children: [ - // 语音波形图 - 只在按住时显示 - if (isPushToTalkActive) ...[ - const AgentSoundBarsWidget(), - const Spacer(), - ], - - // 状态文字和图标 Expanded( - child: Row( - mainAxisAlignment: isPushToTalkActive - ? MainAxisAlignment.end - : MainAxisAlignment.center, - children: [ - Icon( - isPushToTalkActive ? Icons.mic : Icons.mic_none, - size: 16, - color: isPushToTalkActive - ? Colors.blue - : Colors.grey[500], + child: Obx(() { + // 图片模式下使用更高的输入框 + final isImageActive = controller + .isImageInputActive.value; + + return TextField( + controller: + controller.textController, + maxLines: isImageActive + ? 3 + : 5, // 设置合理的最大行数 + minLines: isImageActive ? 2 : 1, + keyboardType: TextInputType + .multiline, // 启用多行键盘 + textInputAction: TextInputAction + .newline, // 支持回车换行 + onTap: () { + // 当点击输入框时,如果'+'选项已展开,则收缩 + if (controller + .isPlusOptionsExpanded + .value) { + controller + .collapsePlusOptions(); + } + }, + style: TextStyle( + fontSize: 15, + color: isDarkMode + ? Colors.white + : Colors.grey[800], ), - const SizedBox(width: 6), - Text( - isPushToTalkActive - ? '正在录音...' - : '按住说话', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: isPushToTalkActive - ? Colors.blue - : Colors.grey[600], + decoration: InputDecoration( + hintText: isImageActive + ? '添加对图片的问题或描述...' + : '输入消息...', + hintStyle: TextStyle( + color: Colors.grey[500], + fontSize: 15, + ), + filled: false, + border: InputBorder.none, + focusedBorder: + OutlineInputBorder( + borderSide: BorderSide( + color: isDarkMode + ? Colors.grey[800]! + : Colors.grey[200]!, + width: 1.0, + ), ), + contentPadding: + EdgeInsets.symmetric( + vertical: isImageActive + ? 12 + : 8), // 调整垂直内边距 ), - ], - ), + onSubmitted: (_) => + controller.sendMessage(), + ); + }), ), ], ), ); - }), - ); - } - }), - ), + } else { + // 语音输入模式 - 按住说话模式 + return Listener( + onPointerDown: (_) { + // 开始按住说话 + controller.startPushToTalk(); + }, + onPointerUp: (_) { + // 结束按住说话 + controller.endPushToTalk(); + }, + // onLongPressCancel: () { + // // 取消按住说话 + // controller.endPushToTalk(); + // }, + child: Obx(() { + final isPushToTalkActive = + controller.isPushToTalkActive.value; + final isListening = + controller.isListening.value; - const SizedBox(width: 8), + return Container( + height: 50, + padding: const EdgeInsets.symmetric( + horizontal: 16), + decoration: BoxDecoration( + color: isPushToTalkActive + ? (isDarkMode + ? Colors.blue[900] + : Colors.blue[100]) + : (isDarkMode + ? Colors.grey[800] + : Colors.grey[200]), + borderRadius: + BorderRadius.circular(22), + border: Border.all( + color: isPushToTalkActive + ? Colors.blue + : (isDarkMode + ? Colors.grey[800]! + : Colors.grey + .withOpacity(0.1)), + width: isPushToTalkActive + ? 2.0 + : 1.0, + ), + ), + child: Row( + children: [ + // 语音波形图 - 只在按住时显示 + if (isPushToTalkActive) ...[ + const AgentSoundBarsWidget(), + //const Spacer(), + ], - // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 - Obx(() { - final hasText = controller.hasTextInput.value; - final hasImage = - controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; - final shouldShowExpand = - controller.shouldShowExpandButton.value; + // 状态文字和图标 + Expanded( + child: Row( + mainAxisAlignment: + isPushToTalkActive + ? MainAxisAlignment + .center + : MainAxisAlignment + .center, + children: [ + Icon( + isPushToTalkActive + ? Icons.mic + : Icons.mic_none, + size: 16, + color: isPushToTalkActive + ? Colors.blue + : Colors.grey[500], + ), + const SizedBox(width: 6), + Text( + isPushToTalkActive + ? '正在录音...' + : '按住说话', + style: TextStyle( + fontSize: 14, + fontWeight: + FontWeight.w500, + color: + isPushToTalkActive + ? Colors.blue + : Colors + .grey[600], + ), + ), + ], + ), + ), + ], + ), + ); + }), + ); + } + }), + ), - if (shouldShowSend) { - // 显示发送按钮(可能还有展开按钮) - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 展开按钮(在发送按钮上方) - if (shouldShowExpand && hasText) - Padding( - padding: - const EdgeInsets.only(bottom: 40), - child: GestureDetector( - onTap: () { - // 打开底部弹出编辑模式 - controller.openFullScreenEdit(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => - const FullScreenEditor(), - ).then((_) { - // 弹出页关闭时的回调 - controller.closeFullScreenEdit(); - }); - }, + const SizedBox(width: 8), + + // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 + Obx(() { + final hasText = controller.hasTextInput.value; + final hasImage = + controller.isImageInputActive.value; + final shouldShowSend = hasText || hasImage; + final shouldShowExpand = + controller.shouldShowExpandButton.value; + + if (shouldShowSend) { + // 显示发送按钮(可能还有展开按钮) + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 展开按钮(在发送按钮上方) + if (shouldShowExpand && hasText) + Padding( + padding: + const EdgeInsets.only(bottom: 40), + child: GestureDetector( + onTap: () { + // 打开底部弹出编辑模式 + controller.openFullScreenEdit(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: + Colors.transparent, + builder: (context) => + const FullScreenEditor(), + ).then((_) { + // 弹出页关闭时的回调 + controller + .closeFullScreenEdit(); + }); + }, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[300], + borderRadius: + BorderRadius.circular(16), + ), + child: Icon( + Icons.expand_less, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 20, + ), + ), + ), + ), + // 发送按钮 + GestureDetector( + onTap: () => controller.sendMessage(), child: Container( - width: 32, - height: 32, + width: 44, + height: 44, decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[700] - : Colors.grey[300], + color: primaryColor, borderRadius: - BorderRadius.circular(16), + BorderRadius.circular(22), ), - child: Icon( - Icons.expand_less, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 20, + child: const Icon( + Icons.send, + color: Colors.white, + size: 22, ), ), ), - ), - // 发送按钮 - GestureDetector( - onTap: () => controller.sendMessage(), - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: primaryColor, - borderRadius: BorderRadius.circular(22), - ), - child: const Icon( - Icons.send, - color: Colors.white, - size: 22, - ), - ), - ), - ], - ); - } else { - // 显示'+'选择按钮 - return Obx(() { - final isExpanded = - controller.isPlusOptionsExpanded.value; - return GestureDetector( - onTap: () { - controller.togglePlusOptions(); - }, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(22), - ), - child: Icon( - isExpanded ? Icons.close : Icons.add, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22, - ), - ), - ); - }); - } - }), - ], - ), - ), - - // 展开的'+'选择选项 - Obx(() { - final isExpanded = - controller.isPlusOptionsExpanded.value; - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - height: isExpanded ? 120.h : 0, // 调整高度以适应按钮尺寸 - child: isExpanded - ? Container( - margin: EdgeInsets.only(top: 8.h), - padding: EdgeInsets.symmetric(horizontal:16.w), // 添加左右边距 - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 改为均匀分布 - children: [ - // 相机按钮 - _buildPlusOptionButton( - context: context, - icon: Icons.camera_alt, - label: '拍照', - onTap: () async { - //controller.collapsePlusOptions(); - await controller.takePhoto(); - }, - isDarkMode: isDarkMode, - ), - // 相册按钮 - _buildPlusOptionButton( - context: context, - icon: Icons.photo_library, - label: '相册', - onTap: () async { - //controller.collapsePlusOptions(); - await controller.pickImage(); - }, - isDarkMode: isDarkMode, - ), - // 文件按钮(预留位置) - // _buildPlusOptionButton( - // context: context, - // icon: Icons.folder, - // label: '文件', - // onTap: () { - // //controller.collapsePlusOptions(); - // // TODO: 实现文件选择功能 - // }, - // isDarkMode: isDarkMode, - // ), - // 通话按钮 - _buildPlusOptionButton( - context: context, - icon: Icons.phone, - label: '打电话', + ], + ); + } else { + // 显示'+'选择按钮 + return Obx(() { + final isExpanded = + controller.isPlusOptionsExpanded.value; + return GestureDetector( onTap: () { - // 启动打电话模式 - controller.startPhoneCall(); + controller.togglePlusOptions(); }, - isDarkMode: isDarkMode, + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], + borderRadius: + BorderRadius.circular(22), + ), + child: Icon( + isExpanded ? Icons.close : Icons.add, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 22, + ), + ), + ); + }); + } + }), + ], + ), + ), + + // 展开的'+'选择选项 + Obx(() { + final isExpanded = + controller.isPlusOptionsExpanded.value; + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: isExpanded ? 120.h : 0, // 调整高度以适应按钮尺寸 + child: isExpanded + ? Container( + margin: EdgeInsets.only(top: 8.h), + padding: EdgeInsets.symmetric( + horizontal: 16.w), // 添加左右边距 + child: Row( + mainAxisAlignment: MainAxisAlignment + .spaceEvenly, // 改为均匀分布 + children: [ + // 相机按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.camera_alt, + label: '拍照', + onTap: () async { + //controller.collapsePlusOptions(); + await controller.takePhoto(); + }, + isDarkMode: isDarkMode, + ), + // 相册按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.photo_library, + label: '相册', + onTap: () async { + //controller.collapsePlusOptions(); + await controller.pickImage(); + }, + isDarkMode: isDarkMode, + ), + // 文件按钮(预留位置) + // _buildPlusOptionButton( + // context: context, + // icon: Icons.folder, + // label: '文件', + // onTap: () { + // //controller.collapsePlusOptions(); + // // TODO: 实现文件选择功能 + // }, + // isDarkMode: isDarkMode, + // ), + // 通话按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.phone, + label: '打电话', + onTap: () { + // 启动打电话模式 + controller.startPhoneCall(); + }, + isDarkMode: isDarkMode, + ), + ], ), - ], - ), - ) - : const SizedBox.shrink(), - ); - }), + ) + : const SizedBox.shrink(), + ); + }), + ], + ), ], ), - ], - ), - ), + ), ], ), @@ -734,12 +767,6 @@ class _AgentViewState extends State with WidgetsBindingObserver { ); } - - - - - - // 构建提示选项芯片 Widget _buildPromptChip({ required BuildContext context, @@ -773,8 +800,6 @@ class _AgentViewState extends State with WidgetsBindingObserver { ); } - - // 构建"+"号展开选项按钮 Widget _buildPlusOptionButton({ required BuildContext context, @@ -923,7 +948,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { children: [ // 音频可视化圆圈 Obx(() { - final isActive = controller.isListening.value || controller.isSpeaking.value; + final isActive = + controller.isListening.value || controller.isSpeaking.value; return AnimatedContainer( duration: const Duration(milliseconds: 300), width: isActive ? 200 : 160, @@ -982,11 +1008,13 @@ class _AgentViewState extends State with WidgetsBindingObserver { children: [ // 静音按钮 Obx(() => _buildPhoneControlButton( - icon: controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, - onTap: () => controller.toggleTts(), - isDarkMode: isDarkMode, - isActive: controller.isTtsEnabled.value, - )), + icon: controller.isTtsEnabled.value + ? Icons.volume_up + : Icons.volume_off, + onTap: () => controller.toggleTts(), + isDarkMode: isDarkMode, + isActive: controller.isTtsEnabled.value, + )), // 主要通话按钮 Obx(() { @@ -1007,7 +1035,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { color: isActive ? Colors.red : Colors.green, boxShadow: [ BoxShadow( - color: (isActive ? Colors.red : Colors.green).withOpacity(0.3), + color: (isActive ? Colors.red : Colors.green) + .withOpacity(0.3), blurRadius: 12, spreadRadius: 2, ), @@ -1050,7 +1079,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { height: 56, decoration: BoxDecoration( shape: BoxShape.circle, - color: backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]), + color: backgroundColor ?? + (isDarkMode ? Colors.grey[800] : Colors.grey[200]), border: isActive ? Border.all(color: Colors.blue, width: 2) : null, ), child: Icon( @@ -1064,7 +1094,3 @@ class _AgentViewState extends State with WidgetsBindingObserver { ); } } - - - - From 63cfcccedeb36a41f362affb9c9d61b3947a6f92 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Thu, 3 Jul 2025 14:22:20 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=99=A8=E5=8D=A1=E7=89=87=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/bindings/initial_binding.dart | 6 +- lib/data/services/music_manager.dart | 2 + .../agent/controllers/agent_controller.dart | 534 +++++++++++++++++- lib/modules/agent/views/music_card.dart | 36 +- lib/modules/music/bindings/music_binding.dart | 15 - .../music/controllers/music_controller.dart | 493 ---------------- .../music/services/music_service_facade.dart | 125 ---- .../music/views/music_playlist_view.dart | 108 ++-- .../music_service/MusicService.kt | 129 +++-- .../music_service/MusicServicePlugin.kt | 8 +- .../music_service/lib/music_service.dart | 13 +- 11 files changed, 701 insertions(+), 768 deletions(-) delete mode 100644 lib/modules/music/bindings/music_binding.dart delete mode 100644 lib/modules/music/controllers/music_controller.dart delete mode 100644 lib/modules/music/services/music_service_facade.dart diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 01564b26a..b916bc88d 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -10,8 +10,6 @@ import '../../data/services/bluetooth_manager.dart'; import '../../data/services/ble_manager.dart'; import '../../data/services/account_service.dart'; import '../../data/services/version_update_service.dart'; -import '../../modules/music/controllers/music_controller.dart'; -import '../../modules/music/services/music_service_facade.dart'; /// 初始绑定,用于管理全局依赖 class InitialBinding extends Bindings { @@ -37,10 +35,8 @@ class InitialBinding extends Bindings { Get.lazyPut(() => BleManager(), fenix: true); // MusicManager Get.lazyPut(() => MusicManager(), fenix: true); - + // 音乐模块 - Get.lazyPut(() => MusicController(), fenix: true); - Get.lazyPut(() => MusicServiceFacade(), fenix: true); Get.lazyPut(() => NavigationManager(), fenix: true); Get.lazyPut(() => LocationManager(), fenix: true); // 火山AI服务 diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index 9bfae0e7a..51975424f 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -105,6 +105,7 @@ class MusicManager extends GetxService { required Function() onPrevious, required Function(int) onSeekTo, required Function() onStop, + required Function(int, int, int, double) onStop1, }) async { try { await _musicService.setMediaSessionCallbacks( @@ -114,6 +115,7 @@ class MusicManager extends GetxService { onPrevious: onPrevious, onSeekTo: onSeekTo, onStop: onStop, + onStop1: onStop1, ); } catch (e) { Logger.d('MusicManager', '设置媒体会话回调失败: $e'); diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index d1ae83ce3..4fd37f1cd 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -25,23 +25,35 @@ import '../../../data/models/message_model.dart'; // 语音输入模式枚举 enum VoiceInputMode { - text, // 文字输入模式 - pushToTalk, // 按住说话模式 - phoneCall, // 打电话模式 + text, // 文字输入模式 + pushToTalk, // 按住说话模式 + phoneCall, // 打电话模式 } class AgentController extends GetxController { static AgentController get to => Get.find(); //单例访问模式 - // 聊天消息列表 final messages = [].obs; // UI控制器 final TextEditingController textController = TextEditingController(); final ScrollController scrollController = ScrollController(); + //音乐播放管理 + final _musiceManager = Get.find(); // 导航管理 final _navigationManager = Get.find(); + RxBool isPlaying = false.obs; + RxInt currentId = 0.obs; + + // 播放进度相关状态 + RxInt currentPosition = 0.obs; // 当前播放位置(毫秒) + RxInt duration = 0.obs; // 总时长(毫秒) + RxDouble progress = 0.0.obs; // 播放进度(0.0-1.0) + + // 当前播放列表管理 + final currentPlaylist = >[].obs; // 当前播放列表 + final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引 // 各种状态标志 final isInitialized = false.obs; @@ -98,6 +110,8 @@ class AgentController extends GetxController { // BLE事件订阅 StreamSubscription? _bleEventSubscription; + // 进度更新定时器 + Timer? _progressTimer; // 用于跟踪AI回复状态 bool _isNewAssistantResponse = true; String _currentResponseId = ''; @@ -110,17 +124,54 @@ class AgentController extends GetxController { // 是否已加载聊天记录 final isHistoryLoaded = false.obs; + // 添加加载状态跟踪 + final RxBool isLoadingMusic = false.obs; + final RxString loadingMusicId = ''.obs; + + // 设置加载状态的方法 + void setLoadingMusic(String id, bool loading) { + loadingMusicId.value = loading ? id : ''; + isLoadingMusic.value = loading; + } + @override void onInit() { super.onInit(); _loadTtsPreference(); //加载TTS偏好设置 _loadChatHistory(); //加载聊天历史记录 _subscribeToEvents(); //订阅代理服务事件 - _subscribeToBleEvents(); //订阅BLE事件 + _initMusicStatus(); //初始化音乐状态 + _setupMediaSessionCallbacks(); //设置媒体会话回调 _setupTextInputListener(); //设置文本输入监听器 } + // 初始化音乐播放状态 + Future _initMusicStatus() async { + try { + await musicStatus(); + Logger.d('AgentController', '音乐状态初始化完成'); + } catch (e) { + Logger.e('AgentController', '音乐状态初始化失败: $e'); + } + } + // 设置媒体会话回调 + void _setupMediaSessionCallbacks() async { + try { + await _musiceManager.setMediaSessionCallbacks( + onPlay: onMediaSessionPlay, + onPause: onMediaSessionPause, + onNext: onMediaSessionNext, + onPrevious: onMediaSessionPrevious, + onSeekTo: onMediaSessionSeekTo, + onStop: onMediaSessionStop, + onStop1: onMediaSessionSeekTo1, + ); + Logger.d('AgentController', '媒体会话回调设置完成'); + } catch (e) { + Logger.e('AgentController', '媒体会话回调设置失败: $e'); + } + } @override void onClose() { @@ -994,7 +1045,8 @@ class AgentController extends GetxController { _pendingPlusOptionsExpansion = false; // 添加小延迟确保UI稳定 Future.delayed(const Duration(milliseconds: 50), () { - if (!isKeyboardVisible.value) { // 再次确认键盘状态 + if (!isKeyboardVisible.value) { + // 再次确认键盘状态 isPlusOptionsExpanded.value = true; } }); @@ -1194,7 +1246,8 @@ class AgentController extends GetxController { } // 检查文字模式与语音模式的一致性 - if (isTextInputMode.value && (isPushToTalkMode.value || isPhoneCallMode.value)) { + if (isTextInputMode.value && + (isPushToTalkMode.value || isPhoneCallMode.value)) { Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式'); isTextInputMode.value = false; } @@ -1268,12 +1321,298 @@ class AgentController extends GetxController { await stopVoiceInput(); } + // 获取音乐播放状态 + Future musicStatus() async { + try { + var status = await _musiceManager.getStatus(); + isPlaying.value = status['isPlaying'] ?? false; + currentId.value = status['id'] ?? 0; + Logger.d('AgentController', + '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); + print("音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}"); + } catch (e) { + Logger.e('AgentController', '获取音乐状态失败: $e'); + } + } + // 播放音乐 + Future musicPlay(Map song) async { + try { + Logger.d(TAG, '开始播放音乐: ${song['title'] ?? song['name']}'); + + // 立即更新UI状态,提供即时反馈 + isPlaying.value = true; + // 修复:确保currentId接收int类型 + final songId = song['id']; + currentId.value = songId is int + ? songId + : (int.tryParse(songId?.toString() ?? '0') ?? 0); + + // 重置进度状态 + currentPosition.value = 0; + duration.value = 0; + progress.value = 0.0; + + final id = song['id'] ?? ""; + final timestampMs = song['ts'] ?? 0; + + // 检查URL是否需要刷新(只有当有时间戳时才检查) + if (timestampMs > 0) { + final now = DateTime.now(); + final tsTime = DateTime.fromMillisecondsSinceEpoch(timestampMs).add( + const Duration(minutes: 10), // 转换为北京时间 + ); + + // 判断是否过期 + if (tsTime.isBefore(now)) { + Logger.d(TAG, '音乐URL已过期,正在获取新URL...'); + try { + var response = await Api.getMusicUrl({ + "ids": [id] + }); + print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据 + final urldata = response['urls'][0] as Map; + song["url"] = urldata['url'] ?? ''; + // 修复:确保currentId接收int类型 + final newSongId = song['id']; + currentId.value = newSongId is int + ? newSongId + : (int.tryParse(newSongId?.toString() ?? '0') ?? 0); + Logger.d(TAG, '获取新URL成功'); + } catch (e) { + Logger.e(TAG, '获取新URL失败: $e'); + // 如果获取新URL失败,尝试使用原URL + } + } + } + + // 转换为MusicManager期望的格式 + final musicData = { + 'id': song['id']?.toString() ?? '', + 'url': song['url']?.toString() ?? '', + 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', + 'artist': + song['artist']?.toString() ?? song['sgener']?.toString() ?? '', + 'coverUrl': + song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', + }; + + // 调用音乐管理器播放 + await _musiceManager.play(musicData); + + // 更新媒体会话元数据和播放状态 + await _updateMediaSessionMetadata(song); + await _updateMediaSessionPlaybackState(true); + // 启动进度定时器 + _startProgressTimer(); + Logger.d(TAG, '音乐播放启动成功'); + } catch (e) { + Logger.e(TAG, '播放音乐失败: $e'); + // 发生错误时重置状态 + isPlaying.value = false; + currentId.value = 0; + } + } + // 暂停音乐 + Future musicPause() async { + try { + Logger.d('AgentController', '暂停音乐播放'); + await _musiceManager.pause(); + isPlaying.value = false; + _stopProgressTimer(); + // 更新媒体会话播放状态 + await _updateMediaSessionPlaybackState(false); + } catch (e) { + Logger.e('AgentController', '暂停音乐失败: $e'); + } + } + + // 恢复音乐播放 + Future musicResume() async { + try { + Logger.d('AgentController', '恢复播放音乐'); + await _musiceManager.resume(); + isPlaying.value = true; + _startProgressTimer(); + // 更新媒体会话播放状态 + await _updateMediaSessionPlaybackState(true); + + Logger.d('AgentController', '音乐恢复播放成功'); + } catch (e) { + Logger.e('AgentController', '恢复播放音乐失败: $e'); + isPlaying.value = false; + } + } + + // 启动进度更新定时器 + 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) { + Logger.d('AgentController', '停止播放进度定时器'); + _progressTimer?.cancel(); + _progressTimer = null; + } + } + + // 获取播放进度 + Future updateMusicProgress() async { + try { + if (!isPlaying.value || currentId.value == 0) { + return; // 如果没有在播放,直接返回 + } + + // var progressInfo = await _musiceManager.getProgress(); + // final newPosition = progressInfo['currentPosition'] ?? 0; + // final newDuration = progressInfo['duration'] ?? 0; + // final newProgress = progressInfo['progress'] ?? 0.0; + + // bool hasUpdate = false; + + // // 只有当值发生变化时才更新,避免不必要的UI刷新 + // if (currentPosition.value != newPosition) { + // currentPosition.value = newPosition; + // hasUpdate = true; + // } + // if (duration.value != newDuration) { + // duration.value = newDuration; + // hasUpdate = true; + // } + // if ((progress.value - newProgress).abs() > 0.001) { + // // 避免浮点数精度问题 + // progress.value = newProgress; + // hasUpdate = true; + // } + + // 如果有进度更新,通知系统媒体会话更新进度信息 + // if (hasUpdate) { + // await _updateMediaSessionProgress(); + // } + } catch (e) { + Logger.e('AgentController', '获取播放进度失败: $e'); + } + } + +// 获取播放进度 + Future updateMusicProgress1(int currentIdInternal, int newPosition, + int newDuration, double newProgress) 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; + print( + "updateMusicProgress1:currentIdInternal:$currentIdInternal newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); + bool hasUpdate = false; + if (currentId.value != currentIdInternal) { + currentId.value = currentIdInternal; + hasUpdate = true; + } + // 只有当值发生变化时才更新,避免不必要的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 _updateMediaSessionProgress() async { + try { + // 这里将调用music_service插件的方法来更新系统媒体会话进度 + // 具体实现将在music_service插件中添加 + await _musiceManager.updateMediaSessionProgress( + currentPosition.value, + duration.value, + progress.value, + ); + } catch (e) { + // 忽略错误,避免影响正常播放 + Logger.d('AgentController', '更新媒体会话进度失败: $e'); + } + } + + /// 更新系统媒体会话的元数据 + Future _updateMediaSessionMetadata(Map song) async { + try { + await _musiceManager.updateMediaSessionMetadata(song); + } catch (e) { + Logger.d('AgentController', '更新媒体会话元数据失败: $e'); + } + } + + /// 更新系统媒体会话的播放状态 + Future _updateMediaSessionPlaybackState(bool isPlaying) async { + try { + await _musiceManager.updateMediaSessionPlaybackState(isPlaying); + } catch (e) { + Logger.d('AgentController', '更新媒体会话播放状态失败: $e'); + } + } + + // 跳转到指定位置 + Future seekToPosition(int positionMs) async { + try { + await _musiceManager.seekTo(positionMs); + // 立即更新进度状态 + // await updateMusicProgress(); + } catch (e) { + Logger.e('AgentController', '跳转播放位置失败: $e'); + } + } + + // 跳转到指定位置 + Future seekToPosition1(int currentIdInternal, int newPosition, + int newDuration, double newProgress) async { + try { + //await _musiceManager.seekTo(positionMs); + // 立即更新进度状态 + await updateMusicProgress1( + currentIdInternal, newPosition, newDuration, newProgress); + } catch (e) { + Logger.e('AgentController', '跳转播放位置失败: $e'); + } + } // 修改自动停止处理,遵循TTS设置 void _handleAutoStop(int index) { @@ -1313,7 +1652,188 @@ class AgentController extends GetxController { ); } + /// 播放音乐列表 + Future playPlaylist(List> playlist) async { + try { + if (playlist.isEmpty) { + Logger.e(TAG, '播放列表为空,无法播放'); + return; + } + + Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲'); + + // 保存当前播放列表 + currentPlaylist.assignAll(playlist); + currentPlaylistIndex.value = 0; + + // 设置加载状态 + isLoadingMusic.value = true; + loadingMusicId.value = playlist[0]['id']?.toString() ?? ''; + + // 重置播放状态 + isPlaying.value = true; + currentPosition.value = 0; + duration.value = 0; + progress.value = 0.0; + + // 格式化歌曲列表数据 + final formattedPlaylist = playlist.map((song) { + return { + 'id': song['id']?.toString() ?? '', + 'url': song['url']?.toString() ?? '', + 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', + 'artist': + song['artist']?.toString() ?? song['sgener']?.toString() ?? '', + 'coverUrl': + song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', + }; + }).toList(); + + // 调用音乐管理器播放列表 + await _musiceManager.playPlaylist(formattedPlaylist); + + // 更新当前播放歌曲ID + if (formattedPlaylist.isNotEmpty) { + currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0; + } + + // 启动进度定时器 + _startProgressTimer(); + + Logger.d(TAG, '音乐列表播放启动成功'); + } catch (e) { + Logger.e(TAG, '播放音乐列表失败: $e'); + isPlaying.value = false; + currentId.value = 0; + + // 显示错误提示 + Get.snackbar( + '播放失败', + '无法播放音乐列表,请稍后重试', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[400], + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } finally { + // 清除加载状态 + isLoadingMusic.value = false; + loadingMusicId.value = ''; + } + } + + /// 播放上一曲 + Future playPrevious() async { + if (currentPlaylist.isEmpty) { + Logger.w(TAG, '当前播放列表为空,无法播放上一曲'); + return; + } + + // 计算上一曲的索引 + int previousIndex = currentPlaylistIndex.value - 1; + if (previousIndex < 0) { + previousIndex = currentPlaylist.length - 1; // 循环到最后一首 + } + + currentPlaylistIndex.value = previousIndex; + final previousSong = currentPlaylist[previousIndex]; + + Logger.d(TAG, '播放上一曲: ${previousSong['name'] ?? previousSong['title']}'); + await musicPlay(previousSong); + } + + /// 播放下一曲 + Future playNext() async { + if (currentPlaylist.isEmpty) { + Logger.w(TAG, '当前播放列表为空,无法播放下一曲'); + return; + } + + // 计算下一曲的索引 + int nextIndex = currentPlaylistIndex.value + 1; + if (nextIndex >= currentPlaylist.length) { + nextIndex = 0; // 循环到第一首 + } + + currentPlaylistIndex.value = nextIndex; + final nextSong = currentPlaylist[nextIndex]; + + Logger.d(TAG, '播放下一曲: ${nextSong['name'] ?? nextSong['title']}'); + await musicPlay(nextSong); + } + + /// 更新当前播放列表和索引(当单独播放某首歌曲时调用) + void updateCurrentPlaylistContext( + List> playlist, int songId) { + currentPlaylist.assignAll(playlist); + + // 查找当前歌曲在列表中的索引 + int index = -1; + for (int i = 0; i < playlist.length; i++) { + if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) { + index = i; + break; + } + } + + if (index != -1) { + currentPlaylistIndex.value = index; + Logger.d(TAG, '更新播放列表上下文,当前歌曲索引: $index'); + } + } + + // ========== 媒体会话回调处理方法 ========== + + /// 处理来自系统通知栏的播放操作 + Future onMediaSessionPlay() async { + Logger.d(TAG, '收到媒体会话播放回调'); + if (!isPlaying.value) { + await musicResume(); + } + } + + /// 处理来自系统通知栏的暂停操作 + Future onMediaSessionPause() async { + Logger.d(TAG, '收到媒体会话暂停回调'); + if (isPlaying.value) { + await musicPause(); + } + } + /// 处理来自系统通知栏的下一曲操作 + Future onMediaSessionNext() async { + Logger.d(TAG, '收到媒体会话下一曲回调'); + await playNext(); + } + /// 处理来自系统通知栏的上一曲操作 + Future onMediaSessionPrevious() async { + Logger.d(TAG, '收到媒体会话上一曲回调'); + await playPrevious(); + } + /// 处理来自系统通知栏的跳转操作 + Future onMediaSessionSeekTo(int positionMs) async { + Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms'); + await seekToPosition(positionMs); + } + + /// 处理来自系统通知栏的停止操作 + Future onMediaSessionStop() async { + Logger.d(TAG, '收到媒体会话停止回调'); + await musicPause(); + // 重置播放状态 + currentPosition.value = 0; + progress.value = 0.0; + _stopProgressTimer(); + } + + /// 处理来自系统通知栏的跳转操作 + Future onMediaSessionSeekTo1(int currentIdInternal, int newPosition, + int newDuration, double newProgress) async { + print( + "onMediaSessionSeekTo1 newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); + await seekToPosition1( + currentIdInternal, newPosition, newDuration, newProgress); + } } diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart index f64564c1d..ecc1e0fe1 100644 --- a/lib/modules/agent/views/music_card.dart +++ b/lib/modules/agent/views/music_card.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../music/services/music_service_facade.dart'; +import '../../../routes/app_routes.dart'; +import '../controllers/agent_controller.dart'; import 'circular_progress_button.dart'; /// 构建音乐项目 Widget buildMusicItem(BuildContext context, int id, String name, String url, String singer, String image) { - final musicFacade = Get.find(); // 获取音乐服务门面 + final controller = Get.find(); // 获取控制器 final isDarkMode = Theme.of(context).brightness == Brightness.dark; - Map song = { 'id': id, 'url': url, @@ -16,7 +16,8 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, 'artist': singer, 'coverUrl': image, }; - // 音乐状态由MusicController自动管理,无需手动刷新 + + controller.musicStatus(); //刷新状态 return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( @@ -82,11 +83,11 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, ), Obx(() { final isThisPlaying = - musicFacade.isPlaying.value && musicFacade.currentId.value == id; + controller.isPlaying.value && controller.currentId.value == id; // 只有当前播放的歌曲才显示进度 final currentProgress = - isThisPlaying ? musicFacade.progress.value : 0.0; + isThisPlaying ? controller.progress.value : 0.0; return CircularProgressButton( isPlaying: isThisPlaying, @@ -97,21 +98,21 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, strokeWidth: 2.0, onPlayPause: () async { try { - if (musicFacade.isPlaying.value) { - if (musicFacade.currentId.value == id) { + if (controller.isPlaying.value) { + if (controller.currentId.value == id) { // 当前歌曲正在播放,暂停它 - await musicFacade.musicPause(); + await controller.musicPause(); } else { // 其他歌曲正在播放,播放这首歌 - await musicFacade.musicPlay(song); + await controller.musicPlay(song); } } else { - if (musicFacade.currentId.value == id) { + if (controller.currentId.value == id) { // 当前歌曲已暂停,恢复播放 - await musicFacade.musicResume(); + await controller.musicResume(); } else { // 播放新歌曲 - await musicFacade.musicPlay(song); + await controller.musicPlay(song); } } } catch (e) { @@ -122,8 +123,8 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, ? (seekProgress) { // 只有当前播放的歌曲才允许跳转 final targetPosition = - (musicFacade.duration.value * seekProgress).round(); - musicFacade.seekToPosition(targetPosition); + (controller.duration.value * seekProgress).round(); + controller.seekToPosition(targetPosition); } : null, ); @@ -137,7 +138,8 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, Widget buildMusicContent( BuildContext context, List> playlist) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + final controller = Get.find(); // 获取控制器 + controller.updateCurrentPlaylistContext(playlist, controller.currentId.value); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -221,7 +223,7 @@ Widget buildMusicContent( child: TextButton( onPressed: () { // 跳转到完整歌单页面,传递完整的歌单数据 - Get.toNamed('/music/playlist', arguments: playlist); + Get.toNamed(Routes.musicPlaylist, arguments: playlist); }, style: TextButton.styleFrom( minimumSize: Size.zero, diff --git a/lib/modules/music/bindings/music_binding.dart b/lib/modules/music/bindings/music_binding.dart deleted file mode 100644 index 231934cfa..000000000 --- a/lib/modules/music/bindings/music_binding.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/music_controller.dart'; - -/// 音乐模块绑定 -/// 负责注册音乐相关的控制器和服务 -class MusicBinding extends Bindings { - @override - void dependencies() { - // 注册音乐控制器为单例 - Get.lazyPut( - () => MusicController(), - fenix: true, // 允许重新创建 - ); - } -} \ No newline at end of file diff --git a/lib/modules/music/controllers/music_controller.dart b/lib/modules/music/controllers/music_controller.dart deleted file mode 100644 index a092d69f7..000000000 --- a/lib/modules/music/controllers/music_controller.dart +++ /dev/null @@ -1,493 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../../../core/utils/logger.dart'; -import '../../../data/services/music_manager.dart'; -import '../../../data/services/network/api.dart'; - -/// 音乐播放控制器 -/// 负责管理音乐播放状态、播放列表和播放控制 -class MusicController extends GetxController { - static const String TAG = 'MusicController'; - - // 音乐播放管理器 - final _musicManager = Get.find(); - - // 播放状态 - final isPlaying = false.obs; - final currentId = 0.obs; - - // 播放进度相关状态 - final currentPosition = 0.obs; // 当前播放位置(毫秒) - final duration = 0.obs; // 总时长(毫秒) - final progress = 0.0.obs; // 播放进度(0.0-1.0) - - // 当前播放列表管理 - final currentPlaylist = >[].obs; // 当前播放列表 - final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引 - - // 加载状态跟踪 - final isLoadingMusic = false.obs; - final loadingMusicId = ''.obs; - - // 进度更新定时器 - Timer? _progressTimer; - - @override - void onInit() { - super.onInit(); - _initMusicStatus(); - _setupMediaSessionCallbacks(); - } - - @override - void onClose() { - _stopProgressTimer(); - super.onClose(); - } - - // 初始化音乐播放状态 - Future _initMusicStatus() async { - try { - await musicStatus(); - Logger.d(TAG, '音乐状态初始化完成'); - } catch (e) { - Logger.e(TAG, '音乐状态初始化失败: $e'); - } - } - - // 设置媒体会话回调 - void _setupMediaSessionCallbacks() async { - try { - await _musicManager.setMediaSessionCallbacks( - onPlay: onMediaSessionPlay, - onPause: onMediaSessionPause, - onNext: onMediaSessionNext, - onPrevious: onMediaSessionPrevious, - onSeekTo: onMediaSessionSeekTo, - onStop: onMediaSessionStop, - ); - Logger.d(TAG, '媒体会话回调设置完成'); - } catch (e) { - Logger.e(TAG, '媒体会话回调设置失败: $e'); - } - } - - // 设置加载状态的方法 - void setLoadingMusic(String id, bool loading) { - loadingMusicId.value = loading ? id : ''; - isLoadingMusic.value = loading; - } - - // 获取音乐播放状态 - Future musicStatus() async { - try { - var status = await _musicManager.getStatus(); - isPlaying.value = status['isPlaying'] ?? false; - currentId.value = status['id'] ?? 0; - Logger.d(TAG, '音乐状态更新: playing=${isPlaying.value}, id=${currentId.value}'); - } catch (e) { - Logger.e(TAG, '获取音乐状态失败: $e'); - } - } - - // 播放音乐 - Future musicPlay(Map song) async { - try { - Logger.d(TAG, '开始播放音乐: ${song['title'] ?? song['name']}'); - - // 立即更新UI状态,提供即时反馈 - isPlaying.value = true; - // 修复:确保currentId接收int类型 - final songId = song['id']; - currentId.value = songId is int - ? songId - : (int.tryParse(songId?.toString() ?? '0') ?? 0); - - // 重置进度状态 - currentPosition.value = 0; - duration.value = 0; - progress.value = 0.0; - - final id = song['id'] ?? ""; - final timestampMs = song['ts'] ?? 0; - - // 检查URL是否需要刷新(只有当有时间戳时才检查) - if (timestampMs > 0) { - final now = DateTime.now(); - final tsTime = DateTime.fromMillisecondsSinceEpoch(timestampMs).add( - const Duration(minutes: 10), // 转换为北京时间 - ); - - // 判断是否过期 - if (tsTime.isBefore(now)) { - Logger.d(TAG, '音乐URL已过期,正在获取新URL...'); - try { - var response = await Api.getMusicUrl({ - "ids": [id] - }); - print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据 - final urldata = response['urls'][0] as Map; - song["url"] = urldata['url'] ?? ''; - // 修复:确保currentId接收int类型 - final newSongId = song['id']; - currentId.value = newSongId is int - ? newSongId - : (int.tryParse(newSongId?.toString() ?? '0') ?? 0); - Logger.d(TAG, '获取新URL成功'); - } catch (e) { - Logger.e(TAG, '获取新URL失败: $e'); - // 如果获取新URL失败,尝试使用原URL - } - } - } - - // 转换为MusicManager期望的格式 - final musicData = { - 'id': song['id']?.toString() ?? '', - 'url': song['url']?.toString() ?? '', - 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', - 'artist': - song['artist']?.toString() ?? song['sgener']?.toString() ?? '', - 'coverUrl': - song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', - }; - - // 调用音乐管理器播放 - await _musicManager.play(musicData); - - // 更新媒体会话元数据和播放状态 - await _updateMediaSessionMetadata(song); - await _updateMediaSessionPlaybackState(true); - - // 启动进度定时器 - _startProgressTimer(); - - Logger.d(TAG, '音乐播放启动成功'); - } catch (e) { - Logger.e(TAG, '播放音乐失败: $e'); - // 发生错误时重置状态 - isPlaying.value = false; - currentId.value = 0; - } - } - - // 暂停音乐 - Future musicPause() async { - try { - Logger.d(TAG, '暂停音乐播放'); - await _musicManager.pause(); - isPlaying.value = false; - _stopProgressTimer(); - - // 更新媒体会话播放状态 - await _updateMediaSessionPlaybackState(false); - } catch (e) { - Logger.e(TAG, '暂停音乐失败: $e'); - } - } - - // 恢复音乐播放 - Future musicResume() async { - try { - Logger.d(TAG, '恢复播放音乐'); - await _musicManager.resume(); - isPlaying.value = true; - _startProgressTimer(); - - // 更新媒体会话播放状态 - await _updateMediaSessionPlaybackState(true); - - Logger.d(TAG, '音乐恢复播放成功'); - } catch (e) { - Logger.e(TAG, '恢复播放音乐失败: $e'); - isPlaying.value = false; - } - } - - // 启动进度更新定时器 - void _startProgressTimer() { - _stopProgressTimer(); // 先停止现有定时器 - - Logger.d(TAG, '启动播放进度定时器'); - _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(TAG, '播放已停止,取消进度定时器'); - } - }); - } - - // 停止进度更新定时器 - void _stopProgressTimer() { - if (_progressTimer != null) { - Logger.d(TAG, '停止播放进度定时器'); - _progressTimer?.cancel(); - _progressTimer = null; - } - } - - // 获取播放进度 - Future updateMusicProgress() async { - try { - if (!isPlaying.value || currentId.value == 0) { - return; // 如果没有在播放,直接返回 - } - - var progressInfo = await _musicManager.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(TAG, '获取播放进度失败: $e'); - } - } - - // 跳转到指定位置 - Future seekToPosition(int positionMs) async { - try { - await _musicManager.seekTo(positionMs); - // 立即更新进度状态 - await updateMusicProgress(); - } catch (e) { - Logger.e(TAG, '跳转播放位置失败: $e'); - } - } - - /// 播放音乐列表 - Future playPlaylist(List> playlist) async { - try { - if (playlist.isEmpty) { - Logger.e(TAG, '播放列表为空,无法播放'); - return; - } - - Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲'); - - // 保存当前播放列表 - currentPlaylist.assignAll(playlist); - currentPlaylistIndex.value = 0; - - // 设置加载状态 - isLoadingMusic.value = true; - loadingMusicId.value = playlist[0]['id']?.toString() ?? ''; - - // 重置播放状态 - isPlaying.value = true; - currentPosition.value = 0; - duration.value = 0; - progress.value = 0.0; - - // 格式化歌曲列表数据 - final formattedPlaylist = playlist.map((song) { - return { - 'id': song['id']?.toString() ?? '', - 'url': song['url']?.toString() ?? '', - 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', - 'artist': - song['artist']?.toString() ?? song['sgener']?.toString() ?? '', - 'coverUrl': - song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', - }; - }).toList(); - - // 调用音乐管理器播放列表 - await _musicManager.playPlaylist(formattedPlaylist); - - // 更新当前播放歌曲ID - if (formattedPlaylist.isNotEmpty) { - currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0; - } - - // 启动进度定时器 - _startProgressTimer(); - - Logger.d(TAG, '音乐列表播放启动成功'); - } catch (e) { - Logger.e(TAG, '播放音乐列表失败: $e'); - isPlaying.value = false; - currentId.value = 0; - - // 显示错误提示 - Get.snackbar( - '播放失败', - '无法播放音乐列表,请稍后重试', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red[400], - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); - } finally { - // 清除加载状态 - isLoadingMusic.value = false; - loadingMusicId.value = ''; - } - } - - /// 播放上一曲 - Future playPrevious() async { - if (currentPlaylist.isEmpty) { - Logger.w(TAG, '当前播放列表为空,无法播放上一曲'); - return; - } - - // 计算上一曲的索引 - int previousIndex = currentPlaylistIndex.value - 1; - if (previousIndex < 0) { - previousIndex = currentPlaylist.length - 1; // 循环到最后一首 - } - - currentPlaylistIndex.value = previousIndex; - final previousSong = currentPlaylist[previousIndex]; - - Logger.d(TAG, '播放上一曲: ${previousSong['name'] ?? previousSong['title']}'); - await musicPlay(previousSong); - } - - /// 播放下一曲 - Future playNext() async { - if (currentPlaylist.isEmpty) { - Logger.w(TAG, '当前播放列表为空,无法播放下一曲'); - return; - } - - // 计算下一曲的索引 - int nextIndex = currentPlaylistIndex.value + 1; - if (nextIndex >= currentPlaylist.length) { - nextIndex = 0; // 循环到第一首 - } - - currentPlaylistIndex.value = nextIndex; - final nextSong = currentPlaylist[nextIndex]; - - Logger.d(TAG, '播放下一曲: ${nextSong['name'] ?? nextSong['title']}'); - await musicPlay(nextSong); - } - - /// 更新当前播放列表和索引(当单独播放某首歌曲时调用) - void updateCurrentPlaylistContext( - List> playlist, int songId) { - currentPlaylist.assignAll(playlist); - - // 查找当前歌曲在列表中的索引 - int index = -1; - for (int i = 0; i < playlist.length; i++) { - if (int.tryParse(playlist[i]['id']?.toString() ?? '0') == songId) { - index = i; - break; - } - } - - if (index != -1) { - currentPlaylistIndex.value = index; - Logger.d(TAG, '更新播放列表上下文,当前歌曲索引: $index'); - } - } - - // ========== 媒体会话回调处理方法 ========== - - /// 处理来自系统通知栏的播放操作 - Future onMediaSessionPlay() async { - Logger.d(TAG, '收到媒体会话播放回调'); - if (!isPlaying.value) { - await musicResume(); - } - } - - /// 处理来自系统通知栏的暂停操作 - Future onMediaSessionPause() async { - Logger.d(TAG, '收到媒体会话暂停回调'); - if (isPlaying.value) { - await musicPause(); - } - } - - /// 处理来自系统通知栏的下一曲操作 - Future onMediaSessionNext() async { - Logger.d(TAG, '收到媒体会话下一曲回调'); - await playNext(); - } - - /// 处理来自系统通知栏的上一曲操作 - Future onMediaSessionPrevious() async { - Logger.d(TAG, '收到媒体会话上一曲回调'); - await playPrevious(); - } - - /// 处理来自系统通知栏的跳转操作 - Future onMediaSessionSeekTo(int positionMs) async { - Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms'); - await seekToPosition(positionMs); - } - - /// 处理来自系统通知栏的停止操作 - Future onMediaSessionStop() async { - Logger.d(TAG, '收到媒体会话停止回调'); - await musicPause(); - // 重置播放状态 - currentPosition.value = 0; - progress.value = 0.0; - _stopProgressTimer(); - } - - // ========== 私有辅助方法 ========== - - /// 更新系统媒体会话的进度信息 - Future _updateMediaSessionProgress() async { - try { - // 具体实现将在music_service插件中添加 - await _musicManager.updateMediaSessionProgress( - currentPosition.value, - duration.value, - progress.value, - ); - } catch (e) { - // 忽略错误,避免影响正常播放 - Logger.d(TAG, '更新媒体会话进度失败: $e'); - } - } - - /// 更新系统媒体会话的元数据 - Future _updateMediaSessionMetadata(Map song) async { - try { - await _musicManager.updateMediaSessionMetadata(song); - } catch (e) { - Logger.d(TAG, '更新媒体会话元数据失败: $e'); - } - } - - /// 更新系统媒体会话的播放状态 - Future _updateMediaSessionPlaybackState(bool isPlaying) async { - try { - await _musicManager.updateMediaSessionPlaybackState(isPlaying); - } catch (e) { - Logger.d(TAG, '更新媒体会话播放状态失败: $e'); - } - } -} \ No newline at end of file diff --git a/lib/modules/music/services/music_service_facade.dart b/lib/modules/music/services/music_service_facade.dart deleted file mode 100644 index 689d249f7..000000000 --- a/lib/modules/music/services/music_service_facade.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/music_controller.dart'; -import '../../../data/services/music_manager.dart'; - -/// 音乐服务门面 -/// 作为音乐功能的统一入口,简化模块间的交互 -class MusicServiceFacade { - // 单例实现 - static final MusicServiceFacade _instance = MusicServiceFacade._internal(); - - factory MusicServiceFacade() => _instance; - - MusicServiceFacade._internal(); - - // 获取音乐控制器实例 - MusicController get musicController => Get.find(); - - // 获取音乐管理器实例 - MusicManager get musicManager => Get.find(); - - /// 播放音乐 - Future musicPlay(Map song) async { - await musicController.musicPlay(song); - } - - /// 暂停音乐 - Future musicPause() async { - await musicController.musicPause(); - } - - /// 恢复播放 - Future musicResume() async { - await musicController.musicResume(); - } - - /// 播放音乐(兼容旧接口) - Future playMusic(Map song) async { - await musicPlay(song); - } - - /// 暂停音乐(兼容旧接口) - Future pauseMusic() async { - await musicPause(); - } - - /// 恢复播放(兼容旧接口) - Future resumeMusic() async { - await musicResume(); - } - - /// 播放/暂停切换 - Future togglePlayPause() async { - if (musicController.isPlaying.value) { - await musicPause(); - } else { - await musicResume(); - } - } - - /// 播放上一曲 - Future playPrevious() async { - await musicController.playPrevious(); - } - - /// 播放下一曲 - Future playNext() async { - await musicController.playNext(); - } - - /// 播放音乐列表 - Future playPlaylist(List> playlist) async { - await musicController.playPlaylist(playlist); - } - - /// 跳转到指定位置 - Future seekToPosition(int positionMs) async { - await musicController.seekToPosition(positionMs); - } - - /// 获取当前播放状态 - bool get isPlayingValue => musicController.isPlaying.value; - - /// 获取当前播放歌曲ID - int get currentIdValue => musicController.currentId.value; - - /// 获取当前播放位置(毫秒) - int get currentPositionValue => musicController.currentPosition.value; - - /// 获取当前歌曲总时长(毫秒) - int get durationValue => musicController.duration.value; - - /// 获取当前播放进度(0.0-1.0) - double get progressValue => musicController.progress.value; - - /// 获取响应式播放状态(用于Obx) - get isPlaying => musicController.isPlaying; - - /// 获取响应式当前播放歌曲ID(用于Obx) - get currentId => musicController.currentId; - - /// 获取响应式当前播放位置(用于Obx) - get currentPosition => musicController.currentPosition; - - /// 获取响应式当前歌曲总时长(用于Obx) - get duration => musicController.duration; - - /// 获取响应式当前播放进度(用于Obx) - get progress => musicController.progress; - - /// 获取当前播放列表 - List> get currentPlaylist => musicController.currentPlaylist; - - /// 获取当前播放索引 - int get currentPlaylistIndex => musicController.currentPlaylistIndex.value; - - /// 设置加载状态 - void setLoadingMusic(String id, bool loading) { - musicController.setLoadingMusic(id, loading); - } - - /// 更新当前播放列表上下文 - void updateCurrentPlaylistContext(List> playlist, int songId) { - musicController.updateCurrentPlaylistContext(playlist, songId); - } -} \ No newline at end of file diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index bda2fb878..69fa15a85 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter/services.dart'; -import '../services/music_service_facade.dart'; import '../../../core/utils/logger.dart'; +import '../../agent/controllers/agent_controller.dart'; import '../../agent/views/circular_progress_button.dart'; class MusicPlaylistView extends StatelessWidget { @@ -13,7 +13,7 @@ class MusicPlaylistView extends StatelessWidget { @override Widget build(BuildContext context) { ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考 - final musicFacade = Get.find(); + final controller = Get.find(); // 获取控制器 final List> playlist = Get.arguments ?? []; final isDarkMode = Theme.of(context).brightness == Brightness.dark; @@ -55,14 +55,14 @@ class MusicPlaylistView extends StatelessWidget { Container( height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例 child: _buildCurrentPlayingSection( - musicFacade, playlist, isDarkMode), + controller, playlist, isDarkMode), ), // 中部:播放全部按钮 - _buildPlayAllSection(musicFacade, playlist, isDarkMode), + _buildPlayAllSection(controller, playlist, isDarkMode), // 下部:歌单列表 - 使用剩余空间 Expanded( child: - _buildPlaylistSection(musicFacade, playlist, isDarkMode), + _buildPlaylistSection(controller, playlist, isDarkMode), ), ], ), @@ -70,19 +70,19 @@ class MusicPlaylistView extends StatelessWidget { } // 构建当前播放音乐详情部分 - Widget _buildCurrentPlayingSection(MusicServiceFacade musicFacade, + Widget _buildCurrentPlayingSection(AgentController controller, List> playlist, bool isDarkMode) { return SingleChildScrollView( // 添加滚动支持 child: Obx(() { // 查找当前播放的歌曲 Map? currentSong; - if (musicFacade.currentId.value != 0) { + if (controller.currentId.value != 0) { try { currentSong = playlist.firstWhere( (song) => int.tryParse(song['id']?.toString() ?? '0') == - musicFacade.currentId.value, + controller.currentId.value, ); } catch (e) { // 如果没找到,使用第一首歌曲 @@ -137,10 +137,10 @@ class MusicPlaylistView extends StatelessWidget { _buildSongInfo(title, artist, isDarkMode), SizedBox(height: 16.h), // 进度条 - _buildProgressBar(musicFacade, isDarkMode), + _buildProgressBar(controller, isDarkMode), SizedBox(height: 15.h), // 播放控制按钮 - _buildPlayControls(musicFacade, playlist, isDarkMode), + _buildPlayControls(controller, playlist, isDarkMode), ], ), ); @@ -149,7 +149,7 @@ class MusicPlaylistView extends StatelessWidget { } // 构建播放全部按钮部分 - Widget _buildPlayAllSection(MusicServiceFacade musicFacade, + Widget _buildPlayAllSection(AgentController controller, List> playlist, bool isDarkMode) { return Container( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), @@ -159,7 +159,7 @@ class MusicPlaylistView extends StatelessWidget { child: ElevatedButton.icon( onPressed: () async { if (playlist.isNotEmpty) { - await musicFacade.playPlaylist(playlist); + await controller.playPlaylist(playlist); } }, icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w), @@ -185,7 +185,7 @@ class MusicPlaylistView extends StatelessWidget { } // 构建歌单列表部分 - Widget _buildPlaylistSection(MusicServiceFacade musicFacade, + Widget _buildPlaylistSection(AgentController controller, List> playlist, bool isDarkMode) { return ListView.builder( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h), @@ -200,7 +200,7 @@ class MusicPlaylistView extends StatelessWidget { return _buildMusicItem( context, - musicFacade, + controller, id, name, url, @@ -214,7 +214,7 @@ class MusicPlaylistView extends StatelessWidget { Widget _buildMusicItem( BuildContext context, - MusicServiceFacade musicFacade, + AgentController controller, String id, String name, String url, @@ -232,7 +232,7 @@ class MusicPlaylistView extends StatelessWidget { return GestureDetector( onTap: () { - _playSelectedSong(musicFacade, song, Get.arguments ?? []); + _playSelectedSong(controller, song, Get.arguments ?? []); }, child: Container( margin: EdgeInsets.only(bottom: 10.h), @@ -324,10 +324,10 @@ class MusicPlaylistView extends StatelessWidget { // 播放按钮 Obx(() { final currentIdInt = int.tryParse(id) ?? 0; - final isThisPlaying = musicFacade.isPlaying.value && - musicFacade.currentId.value == currentIdInt; + final isThisPlaying = controller.isPlaying.value && + controller.currentId.value == currentIdInt; final currentProgress = - isThisPlaying ? musicFacade.progress.value : 0.0; + isThisPlaying ? controller.progress.value : 0.0; return CircularProgressButton( isPlaying: isThisPlaying, @@ -339,20 +339,20 @@ class MusicPlaylistView extends StatelessWidget { onPlayPause: () async { try { Logger.d('MusicPlaylistView', - '播放按钮被点击,当前播放状态: ${musicFacade.isPlaying.value}'); + '播放按钮被点击,当前播放状态: ${controller.isPlaying.value}'); Logger.d('MusicPlaylistView', - '当前歌曲ID: ${musicFacade.currentId.value}, 点击歌曲ID: $currentIdInt'); - if (musicFacade.isPlaying.value) { - if (musicFacade.currentId.value == currentIdInt) { + '当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt'); + if (controller.isPlaying.value) { + if (controller.currentId.value == currentIdInt) { Logger.d('MusicPlaylistView', '暂停当前播放的歌曲'); - await musicFacade.musicPause(); + await controller.musicPause(); } else { Logger.d('MusicPlaylistView', '播放新歌曲'); - await musicFacade.musicPlay(song); + await controller.musicPlay(song); } } else { Logger.d('MusicPlaylistView', '开始播放歌曲'); - await musicFacade.musicPlay(song); + await controller.musicPlay(song); } } catch (e) { Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e'); @@ -362,8 +362,8 @@ class MusicPlaylistView extends StatelessWidget { ? (seekProgress) { // 只有当前播放的歌曲才允许跳转 final targetPosition = - (musicFacade.duration.value * seekProgress).round(); - musicFacade.seekToPosition(targetPosition); + (controller.duration.value * seekProgress).round(); + controller.seekToPosition(targetPosition); } : null, ); @@ -449,14 +449,14 @@ class MusicPlaylistView extends StatelessWidget { } // 构建进度条 - Widget _buildProgressBar(MusicServiceFacade musicFacade, bool isDarkMode) { + Widget _buildProgressBar(AgentController controller, bool isDarkMode) { return Obx(() { return Row( children: [ SizedBox( width: 40.w, child: Text( - _formatDuration(musicFacade.currentPosition.value), + _formatDuration(controller.currentPosition.value), style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -480,11 +480,11 @@ class MusicPlaylistView extends StatelessWidget { overlayShape: const RoundSliderOverlayShape(overlayRadius: 14), ), child: Slider( - value: musicFacade.progress.value.clamp(0.0, 1.0), + value: controller.progress.value.clamp(0.0, 1.0), onChanged: (value) { final targetPosition = - (musicFacade.duration.value * value).round(); - musicFacade.seekToPosition(targetPosition); + (controller.duration.value * value).round(); + controller.seekToPosition(targetPosition); }, ), ), @@ -493,7 +493,7 @@ class MusicPlaylistView extends StatelessWidget { SizedBox( width: 40.w, child: Text( - _formatDuration(musicFacade.duration.value), + _formatDuration(controller.duration.value), style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -508,14 +508,14 @@ class MusicPlaylistView extends StatelessWidget { } // 构建播放控制按钮 - Widget _buildPlayControls(MusicServiceFacade musicFacade, + Widget _buildPlayControls(AgentController controller, List> playlist, bool isDarkMode) { return Obx(() { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( - onPressed: () => _playPrevious(musicFacade, playlist), + onPressed: () => _playPrevious(controller, playlist), iconSize: 32.w, padding: EdgeInsets.all(8.w), // 增大触摸区域 icon: Icon( @@ -540,10 +540,10 @@ class MusicPlaylistView extends StatelessWidget { ], ), child: IconButton( - onPressed: () => _togglePlayPause(musicFacade), + onPressed: () => _togglePlayPause(controller), padding: EdgeInsets.zero, icon: Icon( - musicFacade.isPlaying.value ? Icons.pause : Icons.play_arrow, + controller.isPlaying.value ? Icons.pause : Icons.play_arrow, size: 28.w, color: Colors.white, ), @@ -551,7 +551,7 @@ class MusicPlaylistView extends StatelessWidget { ), SizedBox(width: 16.w), IconButton( - onPressed: () => _playNext(musicFacade, playlist), + onPressed: () => _playNext(controller, playlist), iconSize: 32.w, padding: EdgeInsets.all(8.w), // 增大触摸区域 icon: Icon( @@ -574,35 +574,35 @@ class MusicPlaylistView extends StatelessWidget { } // 播放/暂停切换 - Future _togglePlayPause(MusicServiceFacade musicFacade) async { - if (musicFacade.isPlaying.value) { - await musicFacade.musicPause(); + Future _togglePlayPause(AgentController controller) async { + if (controller.isPlaying.value) { + await controller.musicPause(); } else { - await musicFacade.musicResume(); + await controller.musicResume(); } } // 播放上一曲 void _playPrevious( - MusicServiceFacade musicFacade, List> playlist) { - musicFacade.updateCurrentPlaylistContext( - playlist, musicFacade.currentId.value); - musicFacade.playPrevious(); + AgentController controller, List> playlist) { + controller.updateCurrentPlaylistContext( + playlist, controller.currentId.value); + controller.playPrevious(); } // 播放下一曲 void _playNext( - MusicServiceFacade musicFacade, List> playlist) { - musicFacade.updateCurrentPlaylistContext( - playlist, musicFacade.currentId.value); - musicFacade.playNext(); + AgentController controller, List> playlist) { + controller.updateCurrentPlaylistContext( + playlist, controller.currentId.value); + controller.playNext(); } // 播放选中的歌曲 - void _playSelectedSong(MusicServiceFacade musicFacade, Map song, + void _playSelectedSong(AgentController controller, Map song, List> playlist) { final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0; - musicFacade.updateCurrentPlaylistContext(playlist, songId); - musicFacade.musicPlay(song); + controller.updateCurrentPlaylistContext(playlist, songId); + controller.musicPlay(song); } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index e1d38f7e4..84964300a 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -1,5 +1,6 @@ package com.yunqiinnovation.music_service +import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -13,7 +14,9 @@ import android.media.AudioManager import android.media.MediaPlayer import android.os.Build import android.os.Bundle +import android.os.Handler import android.os.IBinder +import android.os.Looper import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat @@ -47,36 +50,36 @@ class MusicService : MediaBrowserServiceCompat() { fun isPlaying() = currentStatus fun getCurrentId(): Int? = currentIdInternal - // 获取播放进度信息 - fun getProgress(): Map { - return if (instance != null && instance!!.mediaPlayer != null) { - val player = instance!!.mediaPlayer!! - try { - val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0 - val duration = player.duration - val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0 - - mapOf( - "currentPosition" to currentPosition, - "duration" to duration, - "progress" to progress - ) - } catch (e: Exception) { - Log.e(TAG, "获取播放进度失败: ${e.message}") - mapOf( - "currentPosition" to 0, - "duration" to 0, - "progress" to 0.0 - ) - } - } else { - mapOf( - "currentPosition" to 0, - "duration" to 0, - "progress" to 0.0 - ) - } - } + // // 获取播放进度信息 + // fun getProgress(): Map { + // return if (instance != null && instance!!.mediaPlayer != null) { + // val player = instance!!.mediaPlayer!! + // try { + // val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0 + // val duration = player.duration + // val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0 + + // mapOf( + // "currentPosition" to currentPosition, + // "duration" to duration, + // "progress" to progress + // ) + // } catch (e: Exception) { + // Log.e(TAG, "获取播放进度失败: ${e.message}") + // mapOf( + // "currentPosition" to 0, + // "duration" to 0, + // "progress" to 0.0 + // ) + // } + // } else { + // mapOf( + // "currentPosition" to 0, + // "duration" to 0, + // "progress" to 0.0 + // ) + // } + // } private var instance: MusicService? = null private var flutterChannel: io.flutter.plugin.common.MethodChannel? = null @@ -118,7 +121,7 @@ class MusicService : MediaBrowserServiceCompat() { 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? Int ?: song["id"] as? Int ?: 0 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) @@ -166,6 +169,8 @@ class MusicService : MediaBrowserServiceCompat() { private lateinit var mediaSession: MediaSessionCompat private lateinit var audioManager: AudioManager private var mediaPlayer: MediaPlayer? = null + private val progressHandler = Handler(Looper.getMainLooper()) + private lateinit var progressUpdater: Runnable override fun onCreate() { super.onCreate() @@ -191,11 +196,35 @@ class MusicService : MediaBrowserServiceCompat() { registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED) } else { registerReceiver(controlReceiver, filter) + } + // 初始化进度更新任务 + progressUpdater = Runnable { + updateProgress() + progressHandler.postDelayed(progressUpdater, 1000) // 每秒更新一次 } isReceiverRegistered = true Log.i(TAG, "MediaService 启动完成") } - +@SuppressLint("SuspiciousIndentation") +private fun updateProgress() { + mediaPlayer?.let { player -> + if (player.isPlaying || currentStatus) { + val currentPosition = player.currentPosition + val duration = player.duration + val progress = if (duration > 0) currentPosition.toFloat() / duration else 0f + // 更新媒体会话进度 + + // 通知Flutter层 + flutterChannel?.invokeMethod("onMediaSessionSeekTo1", + mapOf( + "currentIdInternal" to currentIdInternal, + "currentPosition" to currentPosition, + "duration" to duration, + "progress" to progress + )) + } + } + } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.i(TAG, "onStartCommand: $intent") intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let { @@ -210,7 +239,7 @@ class MusicService : MediaBrowserServiceCompat() { val songMap = intent.getSerializableExtra("song") as? Map songMap?.let { song -> val songId = song["id"] ?: return@let - + // currentIdInternal = songId val existingIndex = playlist.indexOfFirst { it["id"] == songId } if (existingIndex == -1) { playlist.add(song) @@ -323,9 +352,9 @@ class MusicService : MediaBrowserServiceCompat() { } override fun onSeekTo(pos: Long) { Log.i(TAG, "onSeekTo 通知: ${pos}ms") - seekTo(pos.toInt()) - // 通知Flutter层 - flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt())) + // seekTo(pos.toInt()) + // // 通知Flutter层 + // flutterChannel?.invokeMethod("onMediaSessionSeekTo", mapOf("position" to pos.toInt())) } override fun onStop() { Log.i(TAG, "onStop 通知!") @@ -531,9 +560,13 @@ class MusicService : MediaBrowserServiceCompat() { setOnPreparedListener { start() updateNotification(title,artist,coverUrl) + // 启动进度更新 + progressHandler.post(progressUpdater) Log.i(TAG, "开始播放: $url") } - setOnCompletionListener { playNextOrStop() } + setOnCompletionListener { // 停止进度更新 + progressHandler.removeCallbacks(progressUpdater) + playNextOrStop() } setOnErrorListener { _, what, extra -> Log.e(TAG, "播放错误 what=$what extra=$extra") true @@ -547,17 +580,17 @@ class MusicService : MediaBrowserServiceCompat() { mediaPlayer?.takeIf { it.isPlaying }?.apply { pause() currentStatus = false - mediaSession.setPlaybackState( - PlaybackStateCompat.Builder() - .setState(PlaybackStateCompat.STATE_PAUSED, currentPosition.toLong(), 1.0f) - .build() - ) + val song = playlist[currentIndex] val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String + val id = song["id"] as? Int ?: 0 + currentIdInternal = id updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PAUSED); + // 停止进度更新 + progressHandler.removeCallbacks(progressUpdater) Log.i(TAG, "已暂停") } @@ -567,17 +600,17 @@ class MusicService : MediaBrowserServiceCompat() { mediaPlayer?.takeIf { !it.isPlaying }?.apply { start() currentStatus = true - mediaSession.setPlaybackState( - PlaybackStateCompat.Builder() - .setState(PlaybackStateCompat.STATE_PLAYING, currentPosition.toLong(), 1.0f) - .build() - ) + val song = playlist[currentIndex] val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String + val id = song["id"] as? Int ?: 0 + currentIdInternal = id updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + // 重启进度更新 + progressHandler.post(progressUpdater) // Log.i(TAG, "继续播放") } } @@ -635,6 +668,8 @@ class MusicService : MediaBrowserServiceCompat() { unregisterReceiver(controlReceiver) isReceiverRegistered = false } + // 停止进度更新 + progressHandler.removeCallbacks(progressUpdater) mediaPlayer?.release() stopForeground(true) isRunning.set(false) diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt index 0c2fe1256..9d5f1043b 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt @@ -70,10 +70,10 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { ) result.success(status) } - "getProgress" -> { - val progressInfo = MusicService.getProgress() - result.success(progressInfo) - } + // "getProgress" -> { + // // val progressInfo = MusicService.getProgress() + // result.success(progressInfo) + // } "seekTo" -> { val position = call.argument("position") if (position != null) { diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index b9d43552e..4c59cd5c0 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -82,7 +82,10 @@ class MusicService { /// 更新媒体会话进度信息 Future updateMediaSessionProgress( - int currentPosition, int duration, double progress) async { + int currentPosition, + int duration, + double progress, + ) async { await _methodChannel.invokeMethod('updateMediaSessionProgress', { 'currentPosition': currentPosition, 'duration': duration, @@ -98,6 +101,7 @@ class MusicService { required Function() onPrevious, required Function(int) onSeekTo, required Function() onStop, + required Function(int, int, int, double) onStop1, }) async { // 设置方法调用处理器来处理来自原生平台的回调 _methodChannel.setMethodCallHandler((call) async { @@ -121,6 +125,13 @@ class MusicService { case 'onMediaSessionStop': onStop(); break; + case 'onMediaSessionSeekTo1': + 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); + break; } }); From 0ad22cbb6a8ba7bdcbe9b709731302c83f91f9d7 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Thu, 3 Jul 2025 14:24:24 +0800 Subject: [PATCH 3/5] add --- lib/modules/agent/controllers/agent_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 4fd37f1cd..11ce214b8 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -1528,7 +1528,7 @@ class AgentController extends GetxController { print( "updateMusicProgress1:currentIdInternal:$currentIdInternal newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); bool hasUpdate = false; - if (currentId.value != currentIdInternal) { + if (currentId.value != currentIdInternal && currentIdInternal != 0) { currentId.value = currentIdInternal; hasUpdate = true; } From 50c3d1fc0570285181ff7686f1b751e526f22ef9 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Thu, 3 Jul 2025 16:42:02 +0800 Subject: [PATCH 4/5] add --- devtools_options.yaml | 3 +++ lib/data/services/music_manager.dart | 2 +- .../agent/controllers/agent_controller.dart | 19 +++++++-------- lib/modules/agent/views/message_bubble.dart | 9 +++++--- lib/modules/agent/views/music_card.dart | 6 +++-- .../music_service/MusicService.kt | 23 ++++++++++++------- .../music_service/lib/music_service.dart | 5 ++-- 7 files changed, 42 insertions(+), 25 deletions(-) create mode 100644 devtools_options.yaml diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 000000000..fa0b357c4 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index 51975424f..621cd927c 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -105,7 +105,7 @@ class MusicManager extends GetxService { required Function() onPrevious, required Function(int) onSeekTo, required Function() onStop, - required Function(int, int, int, double) onStop1, + required Function(String, int, int, double) onStop1, }) async { try { await _musicService.setMediaSessionCallbacks( diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 11ce214b8..adf43b71a 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -1401,7 +1401,7 @@ class AgentController extends GetxController { await _musiceManager.play(musicData); // 更新媒体会话元数据和播放状态 - await _updateMediaSessionMetadata(song); + await _updateMediaSessionMetadata(musicData); await _updateMediaSessionPlaybackState(true); // 启动进度定时器 @@ -1514,11 +1514,12 @@ class AgentController extends GetxController { } // 获取播放进度 - Future updateMusicProgress1(int currentIdInternal, int newPosition, + Future updateMusicProgress1(String currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { - if (!isPlaying.value || currentId.value == 0) { - return; // 如果没有在播放,直接返回 + if (int.parse(currentIdInternal) != 0) { + isPlaying.value = true; + // return; // 如果没有在播放,直接返回 } // // var progressInfo = await _musiceManager.getProgress(); @@ -1528,8 +1529,8 @@ class AgentController extends GetxController { print( "updateMusicProgress1:currentIdInternal:$currentIdInternal newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); bool hasUpdate = false; - if (currentId.value != currentIdInternal && currentIdInternal != 0) { - currentId.value = currentIdInternal; + if (currentId.value != int.parse(currentIdInternal)) { + currentId.value = int.parse(currentIdInternal); hasUpdate = true; } // 只有当值发生变化时才更新,避免不必要的UI刷新 @@ -1602,7 +1603,7 @@ class AgentController extends GetxController { } // 跳转到指定位置 - Future seekToPosition1(int currentIdInternal, int newPosition, + Future seekToPosition1(String currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { //await _musiceManager.seekTo(positionMs); @@ -1829,10 +1830,10 @@ class AgentController extends GetxController { } /// 处理来自系统通知栏的跳转操作 - Future onMediaSessionSeekTo1(int currentIdInternal, int newPosition, + Future onMediaSessionSeekTo1(String currentIdInternal, int newPosition, int newDuration, double newProgress) async { print( - "onMediaSessionSeekTo1 newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); + "onMediaSessionSeekTo1 currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); await seekToPosition1( currentIdInternal, newPosition, newDuration, newProgress); } diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 0c4e2d782..51f7cf8e2 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -240,9 +240,11 @@ class MessageBubble extends GetView { final musicUrl = card_music['url'] as String? ?? ''; final musicSinger = card_music['sgener'] as String? ?? '未知歌手'; final musicImage = card_music['image'] as String? ?? ''; + final ts = card_music['ts'] as int? ?? 0; + print("music_play card_music: $card_music"); //音乐卡片 - return buildMusicItem( - context, musicId, musicName, musicUrl, musicSinger, musicImage); + return buildMusicItem(context, musicId, musicName, musicUrl, + musicSinger, musicImage, ts); } } else if (meta!.keys.contains('music_playlist')) { final card_musiclist = @@ -281,7 +283,8 @@ class MessageBubble extends GetView { final end = card_navigation['end'] as String? ?? ''; final destination = card_navigation['destination'] as String? ?? ''; final rawList = card_navigation['items'] as List? ?? []; - final list = rawList.map((e) => Map.from(e)).toList(); + final list = + rawList.map((e) => Map.from(e)).toList(); final rawNavigation = card_navigation['navigation']; final Map? navigation = rawNavigation is Map ? rawNavigation : null; diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart index ecc1e0fe1..f4af0c4fb 100644 --- a/lib/modules/agent/views/music_card.dart +++ b/lib/modules/agent/views/music_card.dart @@ -6,7 +6,7 @@ import 'circular_progress_button.dart'; /// 构建音乐项目 Widget buildMusicItem(BuildContext context, int id, String name, String url, - String singer, String image) { + String singer, String image, int ts) { final controller = Get.find(); // 获取控制器 final isDarkMode = Theme.of(context).brightness == Brightness.dark; Map song = { @@ -15,6 +15,7 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, 'title': name, 'artist': singer, 'coverUrl': image, + 'ts': ts, }; controller.musicStatus(); //刷新状态 @@ -204,9 +205,10 @@ Widget buildMusicContent( final url = item["url"] ?? ""; final singer = item["sgener"] ?? "未知歌手"; final image = item["image"] ?? ""; + final ts = item["ts"] ?? 0; return Column( children: [ - buildMusicItem(context, id, name, url, singer, image), + buildMusicItem(context, id, name, url, singer, image, ts), if (index != playlist.length - 1) Divider( height: 1, diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 84964300a..98c400caa 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -38,7 +38,7 @@ class MusicService : MediaBrowserServiceCompat() { private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION" private val isRunning = AtomicBoolean(false) - private var currentIdInternal: Int? = null + private var currentIdInternal: String? = null private var currentStatus = false private var playlist = mutableListOf>() private var currentIndex = 0 @@ -48,7 +48,7 @@ class MusicService : MediaBrowserServiceCompat() { fun isServiceRunning() = isRunning.get() fun isPlaying() = currentStatus - fun getCurrentId(): Int? = currentIdInternal + fun getCurrentId(): String? = currentIdInternal // // 获取播放进度信息 // fun getProgress(): Map { @@ -121,7 +121,8 @@ class MusicService : MediaBrowserServiceCompat() { 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? Int ?: song["id"] as? Int ?: 0 +currentIdInternal = song["id"] as? String ?: song["id"] as? String ?: "0" + 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) @@ -238,8 +239,10 @@ private fun updateProgress() { "play" -> intent.getSerializableExtra("song")?.let { url -> val songMap = intent.getSerializableExtra("song") as? Map songMap?.let { song -> - val songId = song["id"] ?: return@let - // currentIdInternal = songId + val songId = song["id"]as? String ?:"0" + Log.i(TAG, "command songId: $songId") + currentIdInternal = songId + val existingIndex = playlist.indexOfFirst { it["id"] == songId } if (existingIndex == -1) { playlist.add(song) @@ -547,13 +550,15 @@ private fun updateProgress() { private fun play(song: Map) { val url = song["url"] as? String ?: return Log.i(TAG, "开始播放: $url") - val id = song["id"] as? Int ?: 0 + val id = song["id"] as? String ?: "0" + Log.i(TAG, "开始播放play id: $id") val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String mediaPlayer?.release() currentStatus = true currentIdInternal = id + Log.i(TAG, "开始播放play currentIdInternal: $currentIdInternal") mediaPlayer = MediaPlayer().apply { setDataSource(url) setAudioStreamType(AudioManager.STREAM_MUSIC) @@ -585,7 +590,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? Int ?: 0 + val id = song["id"] as? String ?: "0" + Log.i(TAG, "pause id: $id") currentIdInternal = id updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PAUSED); @@ -605,7 +611,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? Int ?: 0 + val id = song["id"] as? String ?: "0" + Log.i(TAG, "resume id: $id") currentIdInternal = id updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PLAYING); diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index 4c59cd5c0..8be33fe68 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -101,7 +101,7 @@ class MusicService { required Function() onPrevious, required Function(int) onSeekTo, required Function() onStop, - required Function(int, int, int, double) onStop1, + required Function(String, int, int, double) onStop1, }) async { // 设置方法调用处理器来处理来自原生平台的回调 _methodChannel.setMethodCallHandler((call) async { @@ -126,7 +126,8 @@ class MusicService { onStop(); break; case 'onMediaSessionSeekTo1': - final currentIdInternal = call.arguments['currentIdInternal'] as int; + final currentIdInternal = + call.arguments['currentIdInternal'] as String; final currentPosition = call.arguments['currentPosition'] as int; final duration = call.arguments['duration'] as int; final progress = call.arguments['progress'] as double; From 8b4b80754e18d95c7100666a73049b47aee74085 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 3 Jul 2025 19:36:40 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E8=A7=A3=E7=BB=91=E9=80=BB=E8=BE=91=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/themes/language/lang/en_us.dart | 2 + lib/core/themes/language/lang/zh_cn.dart | 2 + lib/data/models/user_Info.dart | 6 + lib/data/services/network/api.dart | 9 + .../devices/bindings/devices_binding.dart | 11 + .../controllers/devices_controller.dart | 61 ++++ lib/modules/devices/views/devices_view.dart | 290 ++++++++++++++++++ .../controllers/pairing_controller.dart | 149 +++++---- lib/modules/pairing/views/pairing_view.dart | 47 ++- .../pairing/views/pairing_view_new.dart | 20 +- lib/modules/settings/views/settings_view.dart | 8 +- lib/routes/app_pages.dart | 7 + lib/routes/app_routes.dart | 3 + .../yunqiinnovation/ble_service/BleService.kt | 2 + .../ble_service/BleServicePlugin.kt | 4 + 15 files changed, 562 insertions(+), 59 deletions(-) create mode 100644 lib/modules/devices/bindings/devices_binding.dart create mode 100644 lib/modules/devices/controllers/devices_controller.dart create mode 100644 lib/modules/devices/views/devices_view.dart diff --git a/lib/core/themes/language/lang/en_us.dart b/lib/core/themes/language/lang/en_us.dart index a767d5d7d..e4c912e19 100644 --- a/lib/core/themes/language/lang/en_us.dart +++ b/lib/core/themes/language/lang/en_us.dart @@ -90,4 +90,6 @@ const Map enUS = { "goToBind": "Unbind", "scanQrCode": "Scan the QR code", "endRecording": "Do you want to end the recording?", + "earphone": "Earphone", + "voicerecorder": "VoiceRecorder" }; diff --git a/lib/core/themes/language/lang/zh_cn.dart b/lib/core/themes/language/lang/zh_cn.dart index 07806c5cb..604f31448 100644 --- a/lib/core/themes/language/lang/zh_cn.dart +++ b/lib/core/themes/language/lang/zh_cn.dart @@ -88,4 +88,6 @@ const Map zhCN = { "goToBind": "去绑定", "scanQrCode": "扫描二维码", "endRecording": "你想结束录音吗?", + "earphone": "耳机", + "voicerecorder": "录音笔" }; diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index 63f6d60be..c5be98057 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -21,6 +21,7 @@ class User { String phonemac; String password; int viplv = 0; //添加会员等级字段,0表示普通用户 + String earphonename; String earphonemac; String earphoneauthcode; // final Int64 lastbettime; @@ -41,6 +42,7 @@ class User { required this.phonemac, required this.password, required this.viplv, // 添加默认值 + required this.earphonename, required this.earphonemac, required this.earphoneauthcode, // required this.lastbettime, @@ -70,6 +72,7 @@ class User { phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 + earphonename: (userData?['earphonename'] as String?) ?? '', earphonemac: (userData?['earphonemac'] as String?) ?? '', earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '', // lastbettime: _parseTimestamp(userData['lastbettime']), @@ -97,6 +100,7 @@ class User { password: (userData?['password'] as String?) ?? '', viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 + earphonename: (userData?['earphonename'] as String?) ?? '', earphonemac: (userData?['earphonemac'] as String?) ?? '', earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '', // lastbettime: _parseTimestamp(userData['lastbettime']), @@ -121,6 +125,8 @@ class User { 'phonemac': phonemac, 'password': password, 'viplv': viplv, + + 'earphonename': earphonename, 'earphonemac': earphonemac, 'earphoneauthcode': earphoneauthcode, diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index bed184ddc..0c7235c3b 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -136,6 +136,15 @@ class Api { ); } + //解绑定设备 + static unbindearphone([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/user_unbindearphone', + params: params, + ); + } + //绑定设备 static bindauthcode([params]) { return DioManager().request( diff --git a/lib/modules/devices/bindings/devices_binding.dart b/lib/modules/devices/bindings/devices_binding.dart new file mode 100644 index 000000000..8fb603089 --- /dev/null +++ b/lib/modules/devices/bindings/devices_binding.dart @@ -0,0 +1,11 @@ +import 'package:deep_voice/modules/devices/controllers/devices_controller.dart'; +import 'package:get/get.dart'; + +class DevicesBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => DevicesController(), + ); + } +} diff --git a/lib/modules/devices/controllers/devices_controller.dart b/lib/modules/devices/controllers/devices_controller.dart new file mode 100644 index 000000000..2ca2e8f47 --- /dev/null +++ b/lib/modules/devices/controllers/devices_controller.dart @@ -0,0 +1,61 @@ +import 'package:deep_voice/core/utils/logger.dart'; +import 'package:deep_voice/data/models/user_Info.dart'; +import 'package:deep_voice/data/services/ble_manager.dart'; +import 'package:deep_voice/data/services/network/api.dart'; +import 'package:deep_voice/routes/app_routes.dart'; +import 'package:get/get.dart'; +import 'package:get/get_rx/get_rx.dart'; +import 'package:get/get_state_manager/src/simple/get_controllers.dart'; + +/* +设备管理 +*/ +class DevicesController extends GetxController { + late final BleManager _bleManager; + //耳机绑定 + final isearphone = false.obs; + final earphoneName = "".obs; + final earphoneMac = "".obs; + final earphoneCode = "".obs; + + //录音笔 + final isvoicepen = false.obs; + final voicepenName = "".obs; + final voicepenMac = "".obs; + + @override + void onInit() { + super.onInit(); + _bleManager = Get.find(); + earphoneName.value = User.instance.earphonename; + earphoneMac.value = User.instance.earphonemac; + earphoneCode.value = User.instance.earphoneauthcode; + if (earphoneCode.value != "") { + isearphone.value = true; + } + } + + @override + void onClose() { + super.onClose(); + } + + void unbindearphone() async { + final response = await Api.unbindearphone({ + "code": earphoneCode.value, + "earphonname": earphoneMac.value, + }); + if (response != null) { + User.instance.earphonename = ""; + User.instance.earphonemac = ""; + User.instance.earphoneauthcode = ""; + isearphone.value = false; + _bleManager.clearPairedDevices(); + _bleManager.disconnect(); + Get.toNamed(Routes.pairing); + return; + } else { + return; + } + } +} diff --git a/lib/modules/devices/views/devices_view.dart b/lib/modules/devices/views/devices_view.dart new file mode 100644 index 000000000..d77b08253 --- /dev/null +++ b/lib/modules/devices/views/devices_view.dart @@ -0,0 +1,290 @@ +import 'dart:ui'; + +import 'package:deep_voice/modules/devices/controllers/devices_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class DevicesView extends GetView { + const DevicesView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + // 直接使用 Theme.of(context) 获取当前主题 + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: AppBar( + automaticallyImplyLeading: false, + centerTitle: true, + elevation: 0, + backgroundColor: isDarkMode ? Colors.black : Colors.white, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + onPressed: () { + Get.back(); // 正常返回 + }, + ), + title: Text( + '设备管理'.tr, + style: TextStyle( + fontSize: 20.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + body: ListView( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + children: [ + _buildEarphoneCard(isDarkMode), + SizedBox(height: 20.h), + _buildVoiceRecorderCard(isDarkMode), + SizedBox(height: 20.h), + ], + ), + ); + } + + // 耳机 + Widget _buildEarphoneCard(bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 4.w, bottom: 8.h), + child: Text( + 'earphone'.tr, // AI设置 + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.blue[400] : Colors.blue[600], + letterSpacing: 1, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16.r), + child: BackdropFilter( + filter: isDarkMode + ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Column( + children: [ + Obx( + () => + // 耳机列表 + _buildSimpleNavigationSetting( + title: controller.isearphone.value + ? controller.earphoneName.value + : "未绑定", + subtitle: controller.isearphone.value + ? controller.earphoneMac.value + : "还未绑定设备", + icon: Icons.headphones, + iconBgColor: isDarkMode + ? const Color.fromARGB(255, 255, 255, 255)! + .withOpacity(0.3) + : const Color.fromARGB(255, 0, 0, 0)!, + iconColor: isDarkMode + ? const Color.fromARGB(255, 0, 0, 0)! + : const Color.fromARGB(255, 255, 255, 255)!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + isDarkMode: isDarkMode, + showbutt: controller.isearphone.value, + onPressed: () { + controller.unbindearphone(); + }, + ), + ) + ], + ), + ), + ), + ), + ], + ); + } + + // 录音笔 + Widget _buildVoiceRecorderCard(bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 4.w, bottom: 8.h), + child: Text( + 'voicerecorder'.tr, //AI设置 + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.blue[400] : Colors.blue[600], + letterSpacing: 1, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16.r), + child: BackdropFilter( + filter: isDarkMode + ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Column( + children: [ + // 实时对话入口 + Obx( + () => _buildSimpleNavigationSetting( + title: '未绑定', + subtitle: '还未绑定设备', + icon: Icons.edit, + iconBgColor: isDarkMode + ? const Color.fromARGB(255, 255, 255, 255)! + .withOpacity(0.3) + : const Color.fromARGB(255, 0, 0, 0)!, + iconColor: isDarkMode + ? const Color.fromARGB(255, 0, 0, 0)! + : const Color.fromARGB(255, 255, 255, 255)!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + isDarkMode: isDarkMode, + showbutt: controller.isvoicepen.value, + onPressed: () {}, + ), + ) + ], + ), + ), + ), + ), + ], + ); + } + + // 简单导航设置项 + Widget _buildSimpleNavigationSetting({ + required String title, + String? subtitle, + required IconData icon, + required Color iconBgColor, + required Color iconColor, + Color? titleColor, + Color? subtitleColor, + required bool isDarkMode, + required bool showbutt, + required VoidCallback onPressed, + }) { + return InkWell( + child: Padding( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + icon, + size: 16.w, + color: iconColor, + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: titleColor ?? + (isDarkMode ? Colors.white : Colors.black87), + ), + overflow: TextOverflow.ellipsis, + ), + if (subtitle != null) SizedBox(height: 2.h), + if (subtitle != null) + Text( + subtitle, + style: TextStyle( + fontSize: 12.sp, + color: subtitleColor ?? + (isDarkMode + ? Colors.white70 + : Colors.black54), + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + if (showbutt) + Container( + width: 74.w, + height: 40.w, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: EdgeInsets.symmetric(horizontal: 2, vertical: 2), + ), + onPressed: onPressed, + child: const Text( + '解绑', + style: TextStyle( + color: Colors.white, + ), + ), + )), + ], + ), + ), + ); + } +} diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index e06a8ed5a..f4f3d602e 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:app_settings/app_settings.dart'; +import 'package:deep_voice/data/models/appconfig.dart'; import 'package:deep_voice/data/models/user_Info.dart'; import 'package:deep_voice/data/services/network/api.dart'; import 'package:flutter/material.dart'; @@ -21,6 +22,7 @@ enum PairingState { checking, // 检查中(蓝牙、权限等) waitingClassic, // 等待经典蓝牙连接 verifying, // 验证设备 + binding, //确认绑定设备 manualbinding, //手动绑定设备 associating, // 正在进行BLE设备关联 waitingBle, //等待BLE连接 @@ -61,6 +63,8 @@ class PairingController extends GetxController { final isLoading = false.obs; final showConnectButton = false.obs; final showRetryButton = false.obs; + final showBindButton = false.obs; + final showRetryChectButton = false.obs; final showAutoCodeInput = false.obs; final showSkipButton = false.obs; // 增加临时跳过绑定按钮状态 @@ -68,6 +72,7 @@ class PairingController extends GetxController { final connectButtonText = '连接设备'.obs; final retryButtonText = '重试'.obs; + final deviceCode = ''.obs; // ========== 监听器管理 ========== final List _workers = []; @@ -251,7 +256,8 @@ class PairingController extends GetxController { showSkipBindingButton.value = false; // 重置临时跳过绑定按钮 showSkipButton.value = false; showAutoCodeInput.value = false; - + showBindButton.value = false; + showRetryChectButton.value = false; switch (state) { case PairingState.idle: statusText.value = '准备开始配对'; @@ -266,6 +272,7 @@ class PairingController extends GetxController { statusText.value = '请确保耳机已开启并进入配对模式'; showConnectButton.value = true; showSkipButton.value = true; + connectButtonText.value = '前往蓝牙设置'; break; @@ -273,7 +280,12 @@ class PairingController extends GetxController { statusText.value = '正在验证设备...'; isLoading.value = true; break; - + case PairingState.binding: + statusText.value = '确认绑定'; + isLoading.value = true; + showBindButton.value = true; + showRetryChectButton.value = true; + break; case PairingState.associating: statusText.value = '正在关联设备...'; isLoading.value = true; @@ -418,39 +430,27 @@ class PairingController extends GetxController { Future _verifyDevice(BluetoothDevice device) async { try { Logger.i(_tag, '开始验证设备: ${device.name} (${device.address})'); - // TODO: 实现API验证逻辑 - // 暂时模拟验证通过 - // await Future.delayed(const Duration(seconds: 2));; - if (await authbindingDevice(device)) { - Logger.i(_tag, '设备验证通过'); - _verifiedDeviceAddress = device.address; - // 检查是否已有BLE关联 - final pairedMacAddress = await _bleManager.getPairedMacAddress(); - Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress'); - - if (pairedMacAddress != null && - pairedMacAddress.toUpperCase() == device.address.toUpperCase()) { - // 已有关联,检查BLE连接状态 - Logger.i(_tag, '设备已关联,检查BLE连接状态'); - final currentBleState = await _bleManager.getConnectionState(); - Logger.i(_tag, '验证时BLE状态: $currentBleState'); - - if (currentBleState == 2) { - // BleConnectionState.connected - Logger.i(_tag, 'BLE已连接,直接完成配对'); - _transition(PairingState.completed); - } else { - Logger.i(_tag, 'BLE未连接,等待BLE连接'); - _transition(PairingState.waitingBle); - } + final result = await authbindingDevice(device); + if (result == 2) { + //验证失败 进入手动授权 + await connectble(device); + } else if (result == 1) { + //跳转绑定界面 + //验证失败 进入手动授权 + if (currentDevice != null) { + _transition(PairingState.binding); } else { - // 没有关联或关联的是其他设备,需要进行关联 - Logger.i(_tag, '需要进行BLE设备关联'); - _transition(PairingState.associating); + _transition(PairingState.checking); + _checkCurrentStatus(); } } else { - //验证失败 进入手动授权 - _transition(PairingState.manualbinding); + final authcode_open = AppConfig.env("APP_AUTHCODE_OPEN") ?? ""; + if (authcode_open == "true") { + //验证失败 进入手动授权 + _transition(PairingState.manualbinding); + } else { + _setError(ErrorType.deviceNotMatch, '请连接 DeepSound 设备'); + } } } catch (e) { Logger.e(_tag, '设备验证异常: $e'); @@ -458,6 +458,37 @@ class PairingController extends GetxController { } } + //连接ble + Future connectble(BluetoothDevice device) async { + Logger.i(_tag, '设备验证通过'); + _verifiedDeviceAddress = device.address; + // 检查是否已有BLE关联 + final pairedMacAddress = await _bleManager.getPairedMacAddress(); + Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress'); + + if (pairedMacAddress != null && + pairedMacAddress.toUpperCase() == device.address.toUpperCase()) { + // 已有关联,检查BLE连接状态 + Logger.i(_tag, '设备已关联,检查BLE连接状态'); + final currentBleState = await _bleManager.getConnectionState(); + Logger.i(_tag, '验证时BLE状态: $currentBleState'); + + if (currentBleState == 2) { + // BleConnectionState.connected + Logger.i(_tag, 'BLE已连接,直接完成配对'); + _transition(PairingState.completed); + } else { + Logger.i(_tag, 'BLE未连接,等待BLE连接'); + _transition(PairingState.waitingBle); + } + } else { + // 没有关联或关联的是其他设备,需要进行关联 + Logger.i(_tag, '需要进行BLE设备关联'); + _transition(PairingState.associating); + } + return; + } + /// 开始设备关联 void _startDeviceAssociation() async { try { @@ -524,18 +555,19 @@ class PairingController extends GetxController { } } - //自动绑定设备 - Future authbindingDevice(BluetoothDevice device) async { + //自动绑定设备 0 未合作商 1 合作商 2,已绑定 + Future authbindingDevice(BluetoothDevice device) async { if (User.instance.earphonemac == device.address) { // 已经绑定 - return true; + return 2; } // 检查设备名称是否包含 DeepSound 或 DeapSound - final deviceName = device.name ?? ''; + final deviceName = device.name; print('获取到 deviceName: $deviceName'); if (deviceName.contains('DeepSound') || deviceName.contains('DeapSound')) { await _bleManager.startScan(); + await Future.delayed(const Duration(seconds: 1)); // 尝试最多读取 3 次设备信息,每次间隔 1 秒 Map? deviceinfo; for (int i = 0; i < 3; i++) { @@ -553,27 +585,38 @@ class PairingController extends GetxController { print('获取到 code: $code'); // 安全检查长度 if (code != "") { - final response = await Api.bindearphone({ - "code": code, - "earphonemac": device.address, - }); - Logger.i(_tag, '手动授权结果 ${response}'); - if (response != null) { - Logger.i(_tag, '设备自动绑定成功'); - User.instance.earphonemac = device.address ?? ''; - return true; - } else { - Logger.e(_tag, '手动绑定失败!'); - return false; - } + deviceCode.value = code; + return 1; } else { - return false; + return 0; } } - return false; + return 0; // 调用BLE管理器的自动绑定方法 } else { - return false; + return 0; + } + } + + //确认绑定设备 + void confirmBindDevice() async { + final response = await Api.bindearphone({ + "code": deviceCode.value, + "earphonname": currentDevice?.name, + "earphonemac": currentDevice?.address, + }); + Logger.i(_tag, '手动授权结果 ${response}'); + if (response != null) { + Logger.i(_tag, '绑定设备成功'); + User.instance.earphonemac = currentDevice?.address ?? ''; + User.instance.earphonename = currentDevice?.name ?? ''; + User.instance.earphoneauthcode = deviceCode.value; + _transition(PairingState.verifying); + _verifyDevice(_currentClassicDevice!); + return; + } else { + Logger.e(_tag, '绑定设备失败!'); + return; } } @@ -605,6 +648,8 @@ class PairingController extends GetxController { Logger.i(_tag, '手动授权结果 ${response}'); if (response != null) { User.instance.earphonemac = currentDevice?.address ?? ''; + User.instance.earphonename = currentDevice?.name ?? ''; + User.instance.earphoneauthcode = codeInputController.text.trim(); _transition(PairingState.verifying); _verifyDevice(_currentClassicDevice!); } else { diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index 9594f3760..98c0da59d 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -290,6 +290,7 @@ class PairingView extends GetView { // 当前连接设备(有设备时才显示) if (device != null && (state == PairingState.verifying || + state == PairingState.binding || state == PairingState.waitingBle || state == PairingState.completed)) ...[ SizedBox(height: 24.h), @@ -311,6 +312,8 @@ class PairingView extends GetView { return '请先通过系统蓝牙连接DeapSound设备'; case PairingState.verifying: return '正在验证设备...'; + case PairingState.binding: + return '设备绑定确认...'; case PairingState.associating: return '正在建立设备关联...'; case PairingState.waitingBle: @@ -434,6 +437,8 @@ class PairingView extends GetView { return Obx(() { final showConnectButton = controller.showConnectButton.value; final showRetryButton = controller.showRetryButton.value; + final showBindButton = controller.showBindButton.value; + final showRetryChectButton = controller.showRetryChectButton.value; final showSkipButton = controller.showSkipButton.value; final showAutoCodeInput = controller.showAutoCodeInput.value; final showSkipBindingButton = controller.showSkipBindingButton.value; @@ -470,7 +475,28 @@ class PairingView extends GetView { ], ), ), - + if (showBindButton) + ElevatedButton( + onPressed: () => controller.confirmBindDevice(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + elevation: 0, + minimumSize: Size(double.infinity, 56.h), + ), + child: Text( + "绑定", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox(height: 12.h), // 显示重试按钮 if (showRetryButton) ElevatedButton( @@ -578,6 +604,25 @@ class PairingView extends GetView { ), ), ], + //重新检测 + if (showRetryChectButton) ...[ + SizedBox(height: 12.h), + TextButton( + onPressed: () => controller.onRetryTap(), + style: TextButton.styleFrom( + foregroundColor: Color(0xFF0066CC), + padding: EdgeInsets.symmetric(vertical: 12.h), + minimumSize: Size(double.infinity, 48.h), + ), + child: Text( + '重新扫描'.tr, // 稍后再说 + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ], ), ); diff --git a/lib/modules/pairing/views/pairing_view_new.dart b/lib/modules/pairing/views/pairing_view_new.dart index d4b843744..59026841f 100644 --- a/lib/modules/pairing/views/pairing_view_new.dart +++ b/lib/modules/pairing/views/pairing_view_new.dart @@ -152,7 +152,10 @@ class PairingView extends GetView { iconData = Icons.verified_user; iconColor = Colors.orange; break; - + case PairingState.binding: + iconData = Icons.blind; + iconColor = Colors.blue; + break; case PairingState.associating: iconData = Icons.link; iconColor = Colors.blue; @@ -195,6 +198,7 @@ class PairingView extends GetView { /// 构建按钮 Widget _buildButtons(bool isDarkMode) { final showConnectButton = controller.showConnectButton.value; + final showBindButton = controller.showBindButton.value; final showRetryButton = controller.showRetryButton.value; final showAutoCodeInput = controller.showAutoCodeInput.value; final showSkipButton = controller.showSkipButton.value; @@ -209,6 +213,19 @@ class PairingView extends GetView { isDarkMode, ), + // 绑定按钮 + if (showBindButton) + TextButton( + onPressed: controller.confirmBindDevice, + child: Text( + '绑定', + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ), + if (showRetryButton) _buildPrimaryButton( '重试', @@ -217,6 +234,7 @@ class PairingView extends GetView { ), SizedBox(height: 12.h), + if (showAutoCodeInput) // 自动输入代码按钮 SizedBox( diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 5cce91556..be59cb2bd 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -505,8 +505,8 @@ class SettingsView extends GetView { // 清除配对设备 _buildSimpleNavigationSetting( - title: '清除配对设备', - subtitle: '清除所有BLE配对设备记录', + title: '设备管理', + subtitle: '设备管理列表', icon: Icons.bluetooth_disabled, iconBgColor: isDarkMode ? Colors.red[900]!.withOpacity(0.3) @@ -514,9 +514,7 @@ class SettingsView extends GetView { iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!, titleColor: isDarkMode ? Colors.white : null, subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - controller.clearBleDevices(); - }, + onTap: () => Get.toNamed(Routes.devices), isDarkMode: isDarkMode, ), Divider( diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 1d544a9e7..64eb8dd37 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -1,3 +1,5 @@ +import 'package:deep_voice/modules/devices/bindings/devices_binding.dart'; +import 'package:deep_voice/modules/devices/views/devices_view.dart'; import 'package:get/get.dart'; import '../modules/feedback/bindings/feedback_bindings.dart'; import '../modules/feedback/views/feedback_view.dart'; @@ -233,5 +235,10 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), + GetPage( + name: Routes.devices, + page: () => const DevicesView(), + binding: DevicesBinding(), + ), ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 550ab16cc..2cc2d5e9d 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -52,4 +52,7 @@ abstract class Routes { // 实时语音对话 static const realtime = '/realtime'; + + // 设备管理 + static const devices = '/devices'; } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index b2fbe0cab..fe3584da5 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -231,6 +231,7 @@ object BleService { return true } catch (e: Exception) { Log.e(TAG, "启动扫描失败: ${e.message}", e) + isScanning = false return false } } @@ -255,6 +256,7 @@ object BleService { return true } catch (e: Exception) { Log.e(TAG, "停止扫描失败: ${e.message}", e) + isScanning = false return false } } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index 3e5062717..7cc6932ae 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -304,6 +304,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, Log.d(TAG, "getDeviceInfo $info") result.success(info) } + "disconnect" -> { + BleService.disconnect(); + result.success(null) + } else -> { result.notImplemented() }