Browse Source

ble_service

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
0a370a0226
  1. 9
      lib/core/bindings/initial_binding.dart
  2. 27
      lib/data/services/music_manager.dart
  3. 204
      lib/modules/agent/controllers/agent_controller.dart
  4. 104
      lib/modules/agent/views/agent_view.dart
  5. 132
      lib/modules/agent/views/message_bubble.dart
  6. 2
      local_plugins/agent_service/pubspec.yaml
  7. 33
      local_plugins/music_service/.gitignore
  8. 33
      local_plugins/music_service/.metadata
  9. 3
      local_plugins/music_service/CHANGELOG.md
  10. 1
      local_plugins/music_service/LICENSE
  11. 15
      local_plugins/music_service/README.md
  12. 4
      local_plugins/music_service/analysis_options.yaml
  13. 9
      local_plugins/music_service/android/.gitignore
  14. 70
      local_plugins/music_service/android/build.gradle
  15. 1
      local_plugins/music_service/android/settings.gradle
  16. 11
      local_plugins/music_service/android/src/main/AndroidManifest.xml
  17. 153
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  18. 46
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  19. 19
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt
  20. 38
      local_plugins/music_service/ios/.gitignore
  21. 0
      local_plugins/music_service/ios/Assets/.gitkeep
  22. 19
      local_plugins/music_service/ios/Classes/MusicServicePlugin.swift
  23. 14
      local_plugins/music_service/ios/Resources/PrivacyInfo.xcprivacy
  24. 29
      local_plugins/music_service/ios/music_service.podspec
  25. 55
      local_plugins/music_service/lib/music_service.dart
  26. 72
      local_plugins/music_service/pubspec.yaml
  27. 2
      pubspec.yaml

9
lib/core/bindings/initial_binding.dart

@ -1,3 +1,4 @@
import 'package:deep_voice/data/services/music_manager.dart';
import 'package:get/get.dart';
import '../../data/services/speech_factory.dart';
import '../../data/services/chat_history_service.dart';
@ -8,6 +9,7 @@ import '../../core/utils/logger.dart';
import '../../data/services/open_ai_service_adapter.dart';
import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
@ -34,23 +36,20 @@ class InitialBinding extends Bindings {
adapter.initialize();
return adapter;
}, fenix: true);
// 蓝牙服务
Get.put<BluetoothManager>(BluetoothManager(), permanent: true);
// BleManager
Get.put<BleManager>(BleManager(), permanent: true);
// MusicManager
Get.put<MusicManager>(MusicManager(), permanent: true);
// 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 蓝牙媒体按钮服务
// Get.lazyPut<BluetoothMediaButtonService>(
// () => BluetoothMediaButtonService(),
// fenix: true);
// // 语音交互服务
// Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),

27
lib/data/services/music_manager.dart

@ -0,0 +1,27 @@
import 'package:deep_voice/core/utils/logger.dart';
import 'package:get/get.dart';
import 'package:music_service/music_service.dart';
//音乐播放管理
class MusicManager extends GetxService {
static MusicManager get to => Get.find<MusicManager>();
// 音乐服务实例
final MusicService _musicService = MusicService();
/// 初始化
@override
void onInit() {
super.onInit();
Logger.info('BLE管理器初始化');
}
/// 释放资源
@override
void onClose() {
super.onClose();
}
//播放 音乐
Future<void> play(String url) async {}
}

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

@ -15,6 +15,7 @@ class Message {
final bool isRecognizing; // 是否为语音识别中的临时消息
final bool hasImage; // 是否包含图片
final String? imagePath; // 图片路径
final Map? meta; //卡片数据
Message({
required this.isUser,
@ -23,50 +24,51 @@ class Message {
this.isRecognizing = false,
this.hasImage = false,
this.imagePath,
this.meta,
}) : this.timestamp = timestamp ?? DateTime.now();
}
class AgentController extends GetxController {
static AgentController get to => Get.find();
final logger = Logger();
// 聊天消息列表
final messages = <Message>[].obs;
// 控制器
final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController();
// 状态标志
final isInitialized = false.obs;
final isListening = false.obs;
final isSpeaking = false.obs;
final isProcessing = false.obs;
// 图片处理状态
final isImageProcessing = false.obs;
// 输入模式控制
final isTextInputMode = true.obs;
// 当前输入的文本内容
final currentText = ''.obs;
// 图片输入相关
final isImageInputActive = false.obs; // 是否处于图片输入模式
final selectedImagePath = Rx<String?>(null); // 当前选择的图片路径
// 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription;
// 用于跟踪AI回复状态
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
// 会话ID
final String sessionId = 'default_agent';
// 是否已加载聊天记录
final isHistoryLoaded = false.obs;
@ -86,7 +88,7 @@ class AgentController extends GetxController {
_eventSubscription?.cancel();
super.onClose();
}
// 加载聊天历史记录
Future<void> _loadChatHistory() async {
try {
@ -96,19 +98,21 @@ class AgentController extends GetxController {
page: 1,
pageSize: 20,
);
if (result.messages.isNotEmpty) {
// 转换并添加消息到列表
final historyMessages = result.messages.map((msg) => Message(
isUser: msg.sender == 'user',
text: msg.message,
timestamp: DateTime.parse(msg.timestamp),
)).toList();
final historyMessages = result.messages
.map((msg) => Message(
isUser: msg.sender == 'user',
text: msg.message,
timestamp: DateTime.parse(msg.timestamp),
))
.toList();
// 消息按时间排序,最新的消息在底部
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messages.assignAll(historyMessages);
logger.i('成功加载 ${historyMessages.length} 条聊天记录');
} else {
// 没有历史记录时添加欢迎消息
@ -120,16 +124,15 @@ class AgentController extends GetxController {
_addWelcomeMessage();
} finally {
isHistoryLoaded.value = true;
messages.refresh();
// 在加载完成后直接定位到底部,无需动画
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
}
}
// 添加欢迎消息
void _addWelcomeMessage() {
messages.add(Message(
@ -159,15 +162,16 @@ class AgentController extends GetxController {
isTextInputMode.value = true;
isTextInputMode.refresh();
break;
case AgentServiceEventType.recognizing:
final text = event.data['text'] ?? '';
currentText.value = text; // 保留当前文本,以便其他地方使用
if (text.isNotEmpty) {
// 查找是否有正在识别中的消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
final index = messages
.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
// 更新已有的识别中消息
messages[index] = Message(
@ -188,13 +192,14 @@ class AgentController extends GetxController {
_scrollToBottom();
}
break;
case AgentServiceEventType.recognitionResult:
final text = event.data['text'] ?? '';
// 查找是否有正在识别中的消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (text.isNotEmpty) {
if (index >= 0) {
// 将识别中的消息转换为最终消息
@ -208,7 +213,7 @@ class AgentController extends GetxController {
// 如果没有找到识别中的消息,添加新消息
messages.add(Message(isUser: true, text: text));
}
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
@ -220,11 +225,11 @@ class AgentController extends GetxController {
messages.refresh();
}
break;
case AgentServiceEventType.ttsStarted:
isSpeaking.value = true;
break;
case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled:
@ -238,16 +243,17 @@ class AgentController extends GetxController {
case AgentServiceEventType.imageReady:
// 图片准备就绪,但AI还没有开始处理,保持processing状态
break;
case AgentServiceEventType.assistantToken:
if (!isProcessing.value) isProcessing.value = true;
final token = event.data['token'] ?? '';
final responseId = event.data['responseId'] ?? '';
if (token.isNotEmpty) {
// 如果是新的回复或者响应ID改变,创建新消息
if (_isNewAssistantResponse || (_currentResponseId != responseId && responseId.isNotEmpty)) {
if (_isNewAssistantResponse ||
(_currentResponseId != responseId && responseId.isNotEmpty)) {
messages.add(Message(isUser: false, text: token));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
@ -267,35 +273,37 @@ class AgentController extends GetxController {
_scrollToBottom();
}
break;
case AgentServiceEventType.assistantResponse:
isProcessing.value = false;
isImageProcessing.value = false;
// 标记当前回复完成
_isNewAssistantResponse = true;
break;
case AgentServiceEventType.error:
isListening.value = false;
isSpeaking.value = false;
isProcessing.value = false;
isImageProcessing.value = false;
// 移除临时的识别消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
messages.removeAt(index);
messages.refresh();
}
logger.e('代理服务错误: ${event.data['message']}');
break;
case AgentServiceEventType.autoStop:
isListening.value = false;
// 移除临时的识别消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
// 只有当消息为空或文本太短时才移除
if (messages[index].text.trim().length < 2) {
@ -309,26 +317,51 @@ class AgentController extends GetxController {
timestamp: messages[index].timestamp,
);
messages.refresh();
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text, speakResponse: true);
AgentService.processTextInput(messages[index].text,
speakResponse: true);
}
}
break;
case AgentServiceEventType.functionCall:
// final functionName = event.data['name'] ?? '';
// final arguments = event.data['arguments'] ?? '';
// logger.i('mcp: $functionName, $arguments');
final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? '';
logger.i('Fultter 开始调用 mcp: $functionName, $arguments');
break;
case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? '';
if (!result.isBlank) {
final meta = result["meta"] ?? {};
//不为空
// 如果是新的回复或者响应ID改变,创建新消息
if (_isNewAssistantResponse ||
(_currentResponseId != responseId && responseId.isNotEmpty)) {
messages.add(Message(isUser: false, text: "", meta: meta));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 否则更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text,
meta: meta,
timestamp: messages[lastIndex].timestamp,
);
}
}
}
logger.i('Fultter 结束调用 mcp: $result, $functionCall');
break;
default:
break;
}
@ -359,22 +392,22 @@ class AgentController extends GetxController {
final text = textController.text.trim();
final imagePath = selectedImagePath.value!; // 使用!强制断言非空
await sendImageMessage(imagePath, text: text);
// 清除图片输入状态
clearImageInput();
return;
}
final text = textController.text.trim();
if (text.isEmpty) return;
final message = Message(isUser: true, text: text);
messages.add(message);
textController.clear();
// 滚动到底部
_scrollToBottom();
try {
isProcessing.value = true;
// 标记为新的AI回复
@ -392,31 +425,31 @@ class AgentController extends GetxController {
logger.e('图片不存在: $imagePath');
return;
}
// 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]';
// 添加用户消息
final message = Message(
isUser: true,
isUser: true,
text: displayText,
hasImage: true,
imagePath: imagePath,
);
messages.add(message);
// 清空文本输入
textController.clear();
// 滚动到底部
_scrollToBottom();
try {
isProcessing.value = true;
isImageProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 调用Agent Service处理图片
await AgentService.processImageInput(
imagePath,
@ -433,7 +466,7 @@ class AgentController extends GetxController {
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;
try {
await AgentService.startConversation();
} catch (e) {
@ -444,7 +477,7 @@ class AgentController extends GetxController {
// 停止语音输入
Future<void> stopVoiceInput() async {
if (!isListening.value) return;
try {
await AgentService.stopConversation();
} catch (e) {
@ -456,21 +489,20 @@ class AgentController extends GetxController {
Future<void> clearChat() async {
try {
await AgentService.clearChatHistory();
// 清空本地消息列表
messages.clear();
// 添加欢迎消息
_addWelcomeMessage();
// 重置回复状态
_isNewAssistantResponse = true;
} catch (e) {
logger.e('清除聊天历史失败: $e');
}
}
// 切换输入模式
void toggleInputMode() {
// 如果当前在图片输入模式,先清除
@ -478,9 +510,9 @@ class AgentController extends GetxController {
clearImageInput();
return;
}
isTextInputMode.toggle();
// 切换到语音模式时,直接开始语音输入
if (!isTextInputMode.value) {
startVoiceInput();
@ -498,10 +530,10 @@ class AgentController extends GetxController {
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80, // 设置图片质量
maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度
maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度
);
if (image != null) {
// 设置已选择的图片
selectedImagePath.value = image.path;
@ -520,10 +552,10 @@ class AgentController extends GetxController {
final XFile? photo = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 80, // 设置图片质量
maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度
maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度
);
if (photo != null) {
// 设置已选择的图片
selectedImagePath.value = photo.path;
@ -534,18 +566,18 @@ class AgentController extends GetxController {
logger.e('拍照失败: $e');
}
}
// 清除图片输入状态
void clearImageInput() {
selectedImagePath.value = null;
isImageInputActive.value = false;
}
// 使用预设提示词处理图片
void useImagePrompt(String prompt) {
// 确保selectedImagePath.value不为null
if (selectedImagePath.value == null) return;
// 填充文本框同时直接发送消息
textController.text = prompt;
// 立即发送图片和提示词
@ -554,19 +586,19 @@ class AgentController extends GetxController {
// 发送后清除图片输入状态
clearImageInput();
}
// 预设提示词:总结图片内容
void summarizeImage() {
useImagePrompt('请总结这张图片的内容');
}
// 预设提示词:抽取图像文字
void extractTextFromImage() {
useImagePrompt('请提取这张图片中的所有文字');
}
// 预设提示词:翻译图像文字
void translateImageText() {
useImagePrompt('请翻译这张图片中的文字');
}
}
}

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

@ -10,7 +10,7 @@ class AgentView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.find<AgentController>();
return Scaffold(
appBar: AppBar(
title: Row(
@ -93,12 +93,13 @@ class AgentView extends StatelessWidget {
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
mate: message.meta,
);
},
),
),
),
// 输入区域
Container(
padding: const EdgeInsets.all(16),
@ -117,7 +118,8 @@ class AgentView extends StatelessWidget {
children: [
// 图片预览区域 - 仅在图片输入模式下显示
Obx(() {
if (controller.isImageInputActive.value && controller.selectedImagePath.value != null) {
if (controller.isImageInputActive.value &&
controller.selectedImagePath.value != null) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -154,7 +156,7 @@ class AgentView extends StatelessWidget {
),
],
),
// 预设提示选项
SizedBox(height: 8),
SingleChildScrollView(
@ -188,7 +190,7 @@ class AgentView extends StatelessWidget {
return SizedBox.shrink();
}
}),
// 输入控件行
Row(
children: [
@ -213,14 +215,14 @@ class AgentView extends StatelessWidget {
),
);
}),
const SizedBox(width: 8),
// 文本输入框或语音波形图
Expanded(
child: Obx(() {
final isTextMode = controller.isTextInputMode.value;
if (isTextMode) {
// 文本输入模式 - 注意观察图片模式时的高度变化
return Container(
@ -234,18 +236,23 @@ class AgentView extends StatelessWidget {
Expanded(
child: Obx(() {
// 图片模式下使用更高的输入框
final isImageActive = controller.isImageInputActive.value;
final isImageActive =
controller.isImageInputActive.value;
return TextField(
controller: controller.textController,
maxLines: isImageActive ? 2 : 1,
minLines: isImageActive ? 2 : 1,
decoration: InputDecoration(
hintText: isImageActive ? '添加对图片的问题或描述...' : '输入消息...',
hintText: isImageActive
? '添加对图片的问题或描述...'
: '输入消息...',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: isImageActive ? 12 : 0),
contentPadding: EdgeInsets.symmetric(
vertical: isImageActive ? 12 : 0),
),
onSubmitted: (_) => controller.sendTextMessage(),
onSubmitted: (_) =>
controller.sendTextMessage(),
);
}),
),
@ -271,7 +278,8 @@ class AgentView extends StatelessWidget {
},
child: Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
padding:
const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
@ -281,7 +289,8 @@ class AgentView extends StatelessWidget {
// 语音波形图 - 始终显示
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.start,
children: [
...List.generate(10, (index) {
return _buildSoundBar(index);
@ -289,14 +298,16 @@ class AgentView extends StatelessWidget {
const Spacer(),
// "正在聆听"文字
Obx(() {
final isListening = controller.isListening.value;
final isListening =
controller.isListening.value;
return Row(
children: [
Icon(
Icons.mic,
size: 14,
color: isListening
? Theme.of(context).primaryColor
color: isListening
? Theme.of(context)
.primaryColor
: Colors.grey,
),
const SizedBox(width: 4),
@ -304,8 +315,9 @@ class AgentView extends StatelessWidget {
'正在聆听',
style: TextStyle(
fontSize: 12,
color: isListening
? Theme.of(context).primaryColor
color: isListening
? Theme.of(context)
.primaryColor
: Colors.grey,
),
),
@ -322,9 +334,9 @@ class AgentView extends StatelessWidget {
}
}),
),
const SizedBox(width: 8),
// 图片选择按钮
Container(
width: 48,
@ -351,7 +363,8 @@ class AgentView extends StatelessWidget {
value: 'camera',
child: Row(
children: [
Icon(Icons.camera_alt, color: Theme.of(context).primaryColor),
Icon(Icons.camera_alt,
color: Theme.of(context).primaryColor),
SizedBox(width: 8),
Text('拍照'),
],
@ -361,7 +374,8 @@ class AgentView extends StatelessWidget {
value: 'gallery',
child: Row(
children: [
Icon(Icons.photo_library, color: Theme.of(context).primaryColor),
Icon(Icons.photo_library,
color: Theme.of(context).primaryColor),
SizedBox(width: 8),
Text('从相册选择'),
],
@ -379,7 +393,7 @@ class AgentView extends StatelessWidget {
),
);
}
// 构建提示选项芯片
Widget _buildPromptChip({
required BuildContext context,
@ -408,15 +422,15 @@ class AgentView extends StatelessWidget {
),
);
}
// 构建声音柱
Widget _buildSoundBar(int index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: Obx(() => _SoundBar(
delay: index * 0.1,
isActive: Get.find<AgentController>().isListening.value,
)),
delay: index * 0.1,
isActive: Get.find<AgentController>().isListening.value,
)),
);
}
}
@ -425,41 +439,42 @@ class AgentView extends StatelessWidget {
class _SoundBar extends StatefulWidget {
final double delay;
final bool isActive;
const _SoundBar({
required this.delay,
required this.isActive,
});
@override
_SoundBarState createState() => _SoundBarState();
}
class _SoundBarState extends State<_SoundBar> with SingleTickerProviderStateMixin {
class _SoundBarState extends State<_SoundBar>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 3, end: 15).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
)..addListener(() {
setState(() {});
});
setState(() {});
});
_startOrStopAnimation();
}
@override
void didUpdateWidget(_SoundBar oldWidget) {
super.didUpdateWidget(oldWidget);
@ -467,7 +482,7 @@ class _SoundBarState extends State<_SoundBar> with SingleTickerProviderStateMixi
_startOrStopAnimation();
}
}
void _startOrStopAnimation() {
if (widget.isActive) {
Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () {
@ -481,22 +496,23 @@ class _SoundBarState extends State<_SoundBar> with SingleTickerProviderStateMixi
_controller.animateTo(0.1, duration: Duration(milliseconds: 300));
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: 2,
height: widget.isActive ? _animation.value : 3,
decoration: BoxDecoration(
color: widget.isActive ? Theme.of(context).primaryColor : Colors.grey[400],
color:
widget.isActive ? Theme.of(context).primaryColor : Colors.grey[400],
borderRadius: BorderRadius.circular(1),
),
);
}
}
}

132
lib/modules/agent/views/message_bubble.dart

@ -10,6 +10,7 @@ class MessageBubble extends StatelessWidget {
final bool isRecognizing;
final bool hasImage;
final String? imagePath;
final Map? mate;
const MessageBubble({
Key? key,
@ -19,31 +20,42 @@ class MessageBubble extends StatelessWidget {
this.isRecognizing = false,
this.hasImage = false,
this.imagePath,
this.mate,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final timeFormat = DateFormat('HH:mm');
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisAlignment:
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) _buildAvatar(isUser, context),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
crossAxisAlignment:
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Container(
padding: hasImage ? (message == '[图片]' ? EdgeInsets.zero : const EdgeInsets.all(12)) : const EdgeInsets.all(12),
padding: hasImage
? (message == '[图片]'
? EdgeInsets.zero
: const EdgeInsets.all(12))
: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _getBubbleColor(context),
borderRadius: BorderRadius.circular(18).copyWith(
bottomLeft: isUser ? const Radius.circular(18) : const Radius.circular(4),
bottomRight: !isUser ? const Radius.circular(18) : const Radius.circular(4),
bottomLeft: isUser
? const Radius.circular(18)
: const Radius.circular(4),
bottomRight: !isUser
? const Radius.circular(18)
: const Radius.circular(4),
),
boxShadow: [
BoxShadow(
@ -79,7 +91,7 @@ class MessageBubble extends StatelessWidget {
/// 获取气泡颜色
Color _getBubbleColor(BuildContext context) {
if (isUser) {
return isRecognizing
return isRecognizing
? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色
: Color(0xFF3B82F6); // 使用更明亮的蓝色
} else {
@ -135,7 +147,7 @@ class MessageBubble extends StatelessWidget {
],
);
}
// 如果是图片消息,显示图片
if (hasImage && imagePath != null) {
// 如果消息是默认的[图片]标记,只显示图片
@ -150,7 +162,7 @@ class MessageBubble extends StatelessWidget {
),
);
}
// 如果消息是自定义文本,显示文本+图片
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -177,18 +189,34 @@ class MessageBubble extends StatelessWidget {
],
);
}
// 判断消息内容,显示特殊卡片
if (!isUser) {
if (message.contains('播放列表') || message.contains('音乐')) {
if (!isUser && mate != null) {
if (mate!.keys.contains('card_music')) {
final musicName = mate!['card_music']['name'] as String;
final musicUrl = mate!['card_music']['url'] as String;
final musicSinger = mate!['card_music']['sgener'] as String;
//音乐卡片
return _buildMusicItem(context, musicName, musicUrl, musicSinger);
} else if (mate!.keys.contains('card_musiclist')) {
//音乐播放列表
return _buildMusicContent(context);
} else if (message.contains('景点') || message.contains('旅游')) {
return _buildTravelContent(context);
} else if (message.contains('天气') || message.contains('气温') || message.contains('预报')) {
} else if (mate!.keys.contains('card_weather')) {
//天气
return _buildWeatherContent(context);
}
// if (message.contains('播放列表') || message.contains('音乐')) {
// return _buildMusicContent(context);
// } else if (message.contains('景点') || message.contains('旅游')) {
// return _buildTravelContent(context);
// } else if (message.contains('天气') ||
// message.contains('气温') ||
// message.contains('预报')) {
// return _buildWeatherContent(context);
// }
}
return Text(
message,
style: TextStyle(
@ -272,14 +300,19 @@ class MessageBubble extends StatelessWidget {
],
),
const SizedBox(height: 10),
// 歌曲示例
_buildMusicItem('雨声与钢琴曲', '纯音乐', context),
_buildMusicItem(
context,
'雨声与钢琴曲',
"",
'纯音乐',
),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem('Rainy Day', 'Coldplay', context),
_buildMusicItem(context, 'Rainy Day', "", 'Coldplay'),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem('雨天的爵士咖啡馆', '爵士合集', context),
_buildMusicItem(context, '雨天的爵士咖啡馆', "", '爵士合集'),
const SizedBox(height: 8),
Center(
child: TextButton(
@ -313,7 +346,8 @@ class MessageBubble extends StatelessWidget {
label: Text('播放全部'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
side: BorderSide(
color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@ -344,7 +378,7 @@ class MessageBubble extends StatelessWidget {
],
);
}
/// 构建旅游内容卡片
Widget _buildTravelContent(BuildContext context) {
return Column(
@ -396,14 +430,14 @@ class MessageBubble extends StatelessWidget {
],
),
const SizedBox(height: 10),
// 景点列表
_buildLocationItem('东京塔', '标志性建筑,可俯瞰东京全景', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildLocationItem('涩谷十字路口', '世界上最繁忙的十字路口之一', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildLocationItem('浅草寺', '东京最古老的寺庙', context),
const SizedBox(height: 8),
Center(
child: TextButton(
@ -437,7 +471,8 @@ class MessageBubble extends StatelessWidget {
label: Text('查看地图'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
side: BorderSide(
color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@ -470,7 +505,8 @@ class MessageBubble extends StatelessWidget {
}
/// 构建音乐项目
Widget _buildMusicItem(String title, String artist, BuildContext context) {
Widget _buildMusicItem(
BuildContext context, String name, String url, String singer) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
@ -494,7 +530,7 @@ class MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
@ -504,7 +540,7 @@ class MessageBubble extends StatelessWidget {
),
SizedBox(height: 2),
Text(
artist,
singer,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
@ -516,7 +552,8 @@ class MessageBubble extends StatelessWidget {
),
IconButton(
onPressed: () {},
icon: Icon(Icons.play_circle, color: Theme.of(context).primaryColor),
icon:
Icon(Icons.play_circle, color: Theme.of(context).primaryColor),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 24,
@ -525,9 +562,10 @@ class MessageBubble extends StatelessWidget {
),
);
}
/// 构建地点项目
Widget _buildLocationItem(String name, String description, BuildContext context) {
Widget _buildLocationItem(
String name, String description, BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
@ -749,7 +787,8 @@ class MessageBubble extends StatelessWidget {
label: Text('更多地区'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
side: BorderSide(
color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@ -780,7 +819,7 @@ class MessageBubble extends StatelessWidget {
],
);
}
/// 构建单日天气预报
Widget _buildForecastDay(String day, IconData icon, String temp) {
return Column(
@ -815,26 +854,27 @@ class MessageBubble extends StatelessWidget {
/// 动画点组件
class _AnimatedDot extends StatefulWidget {
final int delayMilliseconds;
const _AnimatedDot({required this.delayMilliseconds});
@override
_AnimatedDotState createState() => _AnimatedDotState();
}
class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderStateMixin {
class _AnimatedDotState extends State<_AnimatedDot>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_animation = Tween<double>(begin: 0, end: 6).animate(
CurvedAnimation(
parent: _controller,
@ -842,22 +882,22 @@ class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderSta
reverseCurve: Interval(0.5, 1.0, curve: Curves.easeIn),
),
)..addListener(() {
setState(() {});
});
setState(() {});
});
Future.delayed(Duration(milliseconds: widget.delayMilliseconds), () {
if (mounted) {
_controller.repeat();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
@ -876,4 +916,4 @@ class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderSta
),
);
}
}
}

2
local_plugins/agent_service/pubspec.yaml

@ -19,6 +19,8 @@ dependencies:
path: ../deepvoice_utils
chat_storage:
path: ../chat_storage
music_service:
path: ../music_service
get: ^4.6.5
dev_dependencies:

33
local_plugins/music_service/.gitignore

@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
build/

33
local_plugins/music_service/.metadata

@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: android
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: ios
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
local_plugins/music_service/CHANGELOG.md

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
local_plugins/music_service/LICENSE

@ -0,0 +1 @@
TODO: Add your license here.

15
local_plugins/music_service/README.md

@ -0,0 +1,15 @@
# music_service
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

4
local_plugins/music_service/analysis_options.yaml

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

9
local_plugins/music_service/android/.gitignore

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

70
local_plugins/music_service/android/build.gradle

@ -0,0 +1,70 @@
group = "com.yunqiinnovation.music_service"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "1.8.22"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.7.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
namespace = "com.yunqiinnovation.music_service"
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
test.java.srcDirs += "src/test/kotlin"
}
defaultConfig {
minSdk = 21
}
dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
}
testOptions {
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}
dependencies {
implementation project(':deepvoice_utils')
}

1
local_plugins/music_service/android/settings.gradle

@ -0,0 +1 @@
rootProject.name = 'music_service'

11
local_plugins/music_service/android/src/main/AndroidManifest.xml

@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.music_service">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application>
<service
android:name=".MusicService"
android:exported="false"
android:enabled="true"
android:foregroundServiceType="mediaPlayback" />
</application>
</manifest>

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

@ -0,0 +1,153 @@
package com.yunqiinnovation.music_service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.yunqiinnovation.music_service.MusicServiceStarter
import kotlinx.coroutines.CoroutineScope
import java.util.concurrent.atomic.AtomicBoolean
class MusicService : Service() {
companion object {
private const val TAG = "MusicService"
private const val CHANNEL_ID = "music_media_service_channel"
private const val NOTIFICATION_ID = 1002
private const val PREF_NAME = "deep_voice_config"
// 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false)
// 获取服务运行状态
fun isServiceRunning(): Boolean = isRunning.get()
}
private var mediaPlayer: MediaPlayer? = null
private val playlist = mutableListOf<String>() // 可扩展支持播放列表
private var currentIndex = 0
override fun onCreate() {
super.onCreate()
val notification = createNotification() // 创建一个非空通知
startForeground(1, notification)
}
private var isPlaying = false
/**
* 创建前台服务通知
*/
private fun createNotification(): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Android 8.0+ 需要创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"音乐播放服务",
NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(channel)
}
// 点击通知打开主界面
val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = if (intent != null) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
null
}
// 构建通知
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound")
.setContentText(if (isPlaying) "正在播放" else "已暂停")
.setSmallIcon(android.R.drawable.ic_media_play)
// 设置点击意图
pendingIntent?.let { builder.setContentIntent(it) }
return builder.build()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
Log.i(TAG, "onStartCommand: $command")
when (command) {
"play" -> {
val url = intent.getStringExtra("url") ?: return START_NOT_STICKY
play(url)
}
"pause" -> mediaPlayer?.pause()
"resume" -> mediaPlayer?.start()
"stop" -> stopSelf()
"next" -> {
currentIndex++
if (currentIndex < playlist.size) play(playlist[currentIndex])
}
"previous" -> {
currentIndex--
if (currentIndex >= 0) play(playlist[currentIndex])
}
"setVolume" -> {
val volume = intent.getFloatExtra("volume", 1.0f)
mediaPlayer?.setVolume(volume, volume)
}
}
return START_STICKY
}
private fun play(url: String) {
Log.i(TAG, "开始播放音乐: $url")
mediaPlayer?.release()
mediaPlayer = MediaPlayer().apply {
setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC)
prepareAsync()
setOnPreparedListener { start() }
setOnCompletionListener { stopSelf() }
}
// startForeground(1, NotificationUtils.createNotification(this, "Playing..."))
}
override fun onDestroy() {
mediaPlayer?.release()
stopForeground(true)
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
/**
* 释放资源
*/
fun release() {
try {
mediaPlayer?.apply {
if (isPlaying) stop()
release()
}
mediaPlayer = null
} catch (e: Exception) {
// 忽略异常
}
}
/**
* 是否正在播放
*/
fun isPlaying() = mediaPlayer?.isPlaying == true
}

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

@ -0,0 +1,46 @@
package com.yunqiinnovation.music_service
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import com.yunqiinnovation.deepvoice_utils.FileLogger
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
/** MusicServicePlugin */
class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
private val TAG = "MusicServicePlugin"
private lateinit var context: Context
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "music_service")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
FileLogger.d(TAG, "Method call: ${call.method}")
val intent = Intent(context, MusicService::class.java)
when (call.method) {
"play" -> {
val url = call.argument<String>("url")
MusicServiceStarter.startServiceWithCommand(context, "play", url)
}
"pause" -> MusicServiceStarter.startServiceWithCommand(context, "pause")
"resume" -> MusicServiceStarter.startServiceWithCommand(context, "resume")
"stop" -> MusicServiceStarter.startServiceWithCommand(context, "stop")
"setVolume" -> {
val volume = call.argument<Double>("volume")?.toFloat()
MusicServiceStarter.startServiceWithCommand(context, "setVolume", volume = volume)
}
else -> result.notImplemented()
}
result.success(null)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {}
}

19
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt

@ -0,0 +1,19 @@
package com.yunqiinnovation.music_service
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
object MusicServiceStarter {
private val TAG = "MusicServiceStarter"
fun startServiceWithCommand(context: Context, command: String, url: String? = null, volume: Float? = null) {
Log.i(TAG, "startServiceWithCommand: $command $url")
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
url?.let { putExtra("url", it) }
volume?.let { putExtra("volume", it) }
}
ContextCompat.startForegroundService(context, intent)
}
}

38
local_plugins/music_service/ios/.gitignore

@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

0
local_plugins/music_service/ios/Assets/.gitkeep

19
local_plugins/music_service/ios/Classes/MusicServicePlugin.swift

@ -0,0 +1,19 @@
import Flutter
import UIKit
public class MusicServicePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "music_service", binaryMessenger: registrar.messenger())
let instance = MusicServicePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
}

14
local_plugins/music_service/ios/Resources/PrivacyInfo.xcprivacy

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

29
local_plugins/music_service/ios/music_service.podspec

@ -0,0 +1,29 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint music_service.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'music_service'
s.version = '0.0.1'
s.summary = 'A new Flutter project.'
s.description = <<-DESC
A new Flutter project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
# If your plugin requires a privacy manifest, for example if it uses any
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'music_service_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
end

55
local_plugins/music_service/lib/music_service.dart

@ -0,0 +1,55 @@
import 'package:flutter/services.dart';
class MusicService {
static final MusicService _instance = MusicService._internal();
/// 获取单例
factory MusicService() => _instance;
// 方法通道
final MethodChannel _methodChannel = const MethodChannel(
'com.yunqiinnovation.music_service',
);
// 事件通道 - 简化为只使用数据和状态通道
late final EventChannel _dataEventChannel;
late final EventChannel _statusEventChannel;
// 事件流
late final Stream<dynamic> _dataStream;
late final Stream<dynamic> _statusStream;
// 获取事件流
Stream<dynamic> get onDataReceived => _dataStream;
// 状态流直接传递原始数据
Stream<dynamic> get onStatusUpdated => _statusStream;
// 内部构造函数
MusicService._internal() {
_dataEventChannel = const EventChannel(
'com.yunqiinnovation.music_service/data_events',
);
_statusEventChannel = const EventChannel(
'com.yunqiinnovation.music_service/status_events',
);
// 初始化事件流
_dataStream = _dataEventChannel.receiveBroadcastStream();
_statusStream = _statusEventChannel.receiveBroadcastStream();
}
Future<void> playMusic(String url) async {
await _methodChannel.invokeMethod('play', {'url': url});
}
Future<void> pauseMusic() async => _methodChannel.invokeMethod('pause');
Future<void> resumeMusic() async => _methodChannel.invokeMethod('resume');
Future<void> nextTrack() async => _methodChannel.invokeMethod('next');
Future<void> previousTrack() async => _methodChannel.invokeMethod('previous');
Future<void> stopMusic() async => _methodChannel.invokeMethod('stop');
Future<void> setVolume(double volume) async => _methodChannel.invokeMethod(
'setVolume',
{'volume': volume},
); // volume: 0.0~1.0
}

72
local_plugins/music_service/pubspec.yaml

@ -0,0 +1,72 @@
name: music_service
description: "A new Flutter project."
version: 0.0.1
homepage:
environment:
sdk: ^3.7.2
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.yunqiinnovation.music_service
pluginClass: MusicServicePlugin
ios:
pluginClass: MusicServicePlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

2
pubspec.yaml

@ -86,6 +86,8 @@ dependencies:
path: local_plugins/classic_bluetooth
agent_service:
path: local_plugins/agent_service
music_service:
path: local_plugins/music_service
dev_dependencies:

Loading…
Cancel
Save