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. 74
      lib/modules/agent/controllers/agent_controller.dart
  4. 54
      lib/modules/agent/views/agent_view.dart
  5. 88
      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 'package:get/get.dart';
import '../../data/services/speech_factory.dart'; import '../../data/services/speech_factory.dart';
import '../../data/services/chat_history_service.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/open_ai_service_adapter.dart';
import '../../data/services/bluetooth_manager.dart'; import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart'; import '../../data/services/ble_manager.dart';
/// 初始绑定,用于管理全局依赖 /// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings { class InitialBinding extends Bindings {
@override @override
@ -35,23 +37,20 @@ class InitialBinding extends Bindings {
return adapter; return adapter;
}, fenix: true); }, fenix: true);
// 蓝牙服务 // 蓝牙服务
Get.put<BluetoothManager>(BluetoothManager(), permanent: true); Get.put<BluetoothManager>(BluetoothManager(), permanent: true);
// BleManager // BleManager
Get.put<BleManager>(BleManager(), permanent: true); Get.put<BleManager>(BleManager(), permanent: true);
// MusicManager
Get.put<MusicManager>(MusicManager(), permanent: true);
// 火山AI服务 // 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true); Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 蓝牙媒体按钮服务 // 蓝牙媒体按钮服务
// Get.lazyPut<BluetoothMediaButtonService>( // Get.lazyPut<BluetoothMediaButtonService>(
// () => BluetoothMediaButtonService(), // () => BluetoothMediaButtonService(),
// fenix: true); // fenix: true);
// // 语音交互服务 // // 语音交互服务
// Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(), // Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
// fenix: true); // fenix: true);

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 {}
}

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

@ -15,6 +15,7 @@ class Message {
final bool isRecognizing; // 是否为语音识别中的临时消息 final bool isRecognizing; // 是否为语音识别中的临时消息
final bool hasImage; // 是否包含图片 final bool hasImage; // 是否包含图片
final String? imagePath; // 图片路径 final String? imagePath; // 图片路径
final Map? meta; //卡片数据
Message({ Message({
required this.isUser, required this.isUser,
@ -23,6 +24,7 @@ class Message {
this.isRecognizing = false, this.isRecognizing = false,
this.hasImage = false, this.hasImage = false,
this.imagePath, this.imagePath,
this.meta,
}) : this.timestamp = timestamp ?? DateTime.now(); }) : this.timestamp = timestamp ?? DateTime.now();
} }
@ -99,11 +101,13 @@ class AgentController extends GetxController {
if (result.messages.isNotEmpty) { if (result.messages.isNotEmpty) {
// 转换并添加消息到列表 // 转换并添加消息到列表
final historyMessages = result.messages.map((msg) => Message( final historyMessages = result.messages
isUser: msg.sender == 'user', .map((msg) => Message(
text: msg.message, isUser: msg.sender == 'user',
timestamp: DateTime.parse(msg.timestamp), text: msg.message,
)).toList(); timestamp: DateTime.parse(msg.timestamp),
))
.toList();
// 消息按时间排序,最新的消息在底部 // 消息按时间排序,最新的消息在底部
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
@ -126,7 +130,6 @@ class AgentController extends GetxController {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.jumpTo(scrollController.position.maxScrollExtent); scrollController.jumpTo(scrollController.position.maxScrollExtent);
}); });
} }
} }
@ -166,7 +169,8 @@ class AgentController extends GetxController {
if (text.isNotEmpty) { if (text.isNotEmpty) {
// 查找是否有正在识别中的消息 // 查找是否有正在识别中的消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); final index = messages
.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) { if (index >= 0) {
// 更新已有的识别中消息 // 更新已有的识别中消息
@ -193,7 +197,8 @@ class AgentController extends GetxController {
final text = event.data['text'] ?? ''; 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 (text.isNotEmpty) {
if (index >= 0) { if (index >= 0) {
@ -247,7 +252,8 @@ class AgentController extends GetxController {
if (token.isNotEmpty) { if (token.isNotEmpty) {
// 如果是新的回复或者响应ID改变,创建新消息 // 如果是新的回复或者响应ID改变,创建新消息
if (_isNewAssistantResponse || (_currentResponseId != responseId && responseId.isNotEmpty)) { if (_isNewAssistantResponse ||
(_currentResponseId != responseId && responseId.isNotEmpty)) {
messages.add(Message(isUser: false, text: token)); messages.add(Message(isUser: false, text: token));
_isNewAssistantResponse = false; _isNewAssistantResponse = false;
_currentResponseId = responseId; _currentResponseId = responseId;
@ -282,7 +288,8 @@ class AgentController extends GetxController {
isImageProcessing.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) { if (index >= 0) {
messages.removeAt(index); messages.removeAt(index);
messages.refresh(); messages.refresh();
@ -295,7 +302,8 @@ class AgentController extends GetxController {
isListening.value = false; 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 (index >= 0) {
// 只有当消息为空或文本太短时才移除 // 只有当消息为空或文本太短时才移除
if (messages[index].text.trim().length < 2) { if (messages[index].text.trim().length < 2) {
@ -313,22 +321,47 @@ class AgentController extends GetxController {
// 发送此消息到AI处理 // 发送此消息到AI处理
isProcessing.value = true; isProcessing.value = true;
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text, speakResponse: true); AgentService.processTextInput(messages[index].text,
speakResponse: true);
} }
} }
break; break;
case AgentServiceEventType.functionCall: case AgentServiceEventType.functionCall:
// final functionName = event.data['name'] ?? ''; final functionName = event.data['name'] ?? '';
// final arguments = event.data['arguments'] ?? ''; final arguments = event.data['arguments'] ?? '';
// logger.i('mcp: $functionName, $arguments'); logger.i('Fultter 开始调用 mcp: $functionName, $arguments');
break; break;
case AgentServiceEventType.functionCallResult: case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? ''; final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? ''; final functionCall = event.data['function_call'] ?? '';
break; 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: default:
break; break;
} }
@ -457,7 +490,6 @@ class AgentController extends GetxController {
try { try {
await AgentService.clearChatHistory(); await AgentService.clearChatHistory();
// 清空本地消息列表 // 清空本地消息列表
messages.clear(); messages.clear();
@ -498,8 +530,8 @@ class AgentController extends GetxController {
final XFile? image = await picker.pickImage( final XFile? image = await picker.pickImage(
source: ImageSource.gallery, source: ImageSource.gallery,
imageQuality: 80, // 设置图片质量 imageQuality: 80, // 设置图片质量
maxWidth: 1024, // 限制最大宽度 maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度 maxHeight: 1024, // 限制最大高度
); );
if (image != null) { if (image != null) {
@ -520,8 +552,8 @@ class AgentController extends GetxController {
final XFile? photo = await picker.pickImage( final XFile? photo = await picker.pickImage(
source: ImageSource.camera, source: ImageSource.camera,
imageQuality: 80, // 设置图片质量 imageQuality: 80, // 设置图片质量
maxWidth: 1024, // 限制最大宽度 maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度 maxHeight: 1024, // 限制最大高度
); );
if (photo != null) { if (photo != null) {

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

@ -93,6 +93,7 @@ class AgentView extends StatelessWidget {
isRecognizing: message.isRecognizing, isRecognizing: message.isRecognizing,
hasImage: message.hasImage, hasImage: message.hasImage,
imagePath: message.imagePath, imagePath: message.imagePath,
mate: message.meta,
); );
}, },
), ),
@ -117,7 +118,8 @@ class AgentView extends StatelessWidget {
children: [ children: [
// 图片预览区域 - 仅在图片输入模式下显示 // 图片预览区域 - 仅在图片输入模式下显示
Obx(() { Obx(() {
if (controller.isImageInputActive.value && controller.selectedImagePath.value != null) { if (controller.isImageInputActive.value &&
controller.selectedImagePath.value != null) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -234,18 +236,23 @@ class AgentView extends StatelessWidget {
Expanded( Expanded(
child: Obx(() { child: Obx(() {
// 图片模式下使用更高的输入框 // 图片模式下使用更高的输入框
final isImageActive = controller.isImageInputActive.value; final isImageActive =
controller.isImageInputActive.value;
return TextField( return TextField(
controller: controller.textController, controller: controller.textController,
maxLines: isImageActive ? 2 : 1, maxLines: isImageActive ? 2 : 1,
minLines: isImageActive ? 2 : 1, minLines: isImageActive ? 2 : 1,
decoration: InputDecoration( decoration: InputDecoration(
hintText: isImageActive ? '添加对图片的问题或描述...' : '输入消息...', hintText: isImageActive
? '添加对图片的问题或描述...'
: '输入消息...',
border: InputBorder.none, 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( child: Container(
height: 48, height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16), padding:
const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200], color: Colors.grey[200],
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
@ -281,7 +289,8 @@ class AgentView extends StatelessWidget {
// 语音波形图 - 始终显示 // 语音波形图 - 始终显示
Expanded( Expanded(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment:
MainAxisAlignment.start,
children: [ children: [
...List.generate(10, (index) { ...List.generate(10, (index) {
return _buildSoundBar(index); return _buildSoundBar(index);
@ -289,14 +298,16 @@ class AgentView extends StatelessWidget {
const Spacer(), const Spacer(),
// "正在聆听"文字 // "正在聆听"文字
Obx(() { Obx(() {
final isListening = controller.isListening.value; final isListening =
controller.isListening.value;
return Row( return Row(
children: [ children: [
Icon( Icon(
Icons.mic, Icons.mic,
size: 14, size: 14,
color: isListening color: isListening
? Theme.of(context).primaryColor ? Theme.of(context)
.primaryColor
: Colors.grey, : Colors.grey,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@ -305,7 +316,8 @@ class AgentView extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: isListening color: isListening
? Theme.of(context).primaryColor ? Theme.of(context)
.primaryColor
: Colors.grey, : Colors.grey,
), ),
), ),
@ -351,7 +363,8 @@ class AgentView extends StatelessWidget {
value: 'camera', value: 'camera',
child: Row( child: Row(
children: [ children: [
Icon(Icons.camera_alt, color: Theme.of(context).primaryColor), Icon(Icons.camera_alt,
color: Theme.of(context).primaryColor),
SizedBox(width: 8), SizedBox(width: 8),
Text('拍照'), Text('拍照'),
], ],
@ -361,7 +374,8 @@ class AgentView extends StatelessWidget {
value: 'gallery', value: 'gallery',
child: Row( child: Row(
children: [ children: [
Icon(Icons.photo_library, color: Theme.of(context).primaryColor), Icon(Icons.photo_library,
color: Theme.of(context).primaryColor),
SizedBox(width: 8), SizedBox(width: 8),
Text('从相册选择'), Text('从相册选择'),
], ],
@ -414,9 +428,9 @@ class AgentView extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1), padding: const EdgeInsets.symmetric(horizontal: 1),
child: Obx(() => _SoundBar( child: Obx(() => _SoundBar(
delay: index * 0.1, delay: index * 0.1,
isActive: Get.find<AgentController>().isListening.value, isActive: Get.find<AgentController>().isListening.value,
)), )),
); );
} }
} }
@ -435,7 +449,8 @@ class _SoundBar extends StatefulWidget {
_SoundBarState createState() => _SoundBarState(); _SoundBarState createState() => _SoundBarState();
} }
class _SoundBarState extends State<_SoundBar> with SingleTickerProviderStateMixin { class _SoundBarState extends State<_SoundBar>
with SingleTickerProviderStateMixin {
late AnimationController _controller; late AnimationController _controller;
late Animation<double> _animation; late Animation<double> _animation;
@ -454,8 +469,8 @@ class _SoundBarState extends State<_SoundBar> with SingleTickerProviderStateMixi
curve: Curves.easeInOut, curve: Curves.easeInOut,
), ),
)..addListener(() { )..addListener(() {
setState(() {}); setState(() {});
}); });
_startOrStopAnimation(); _startOrStopAnimation();
} }
@ -494,7 +509,8 @@ class _SoundBarState extends State<_SoundBar> with SingleTickerProviderStateMixi
width: 2, width: 2,
height: widget.isActive ? _animation.value : 3, height: widget.isActive ? _animation.value : 3,
decoration: BoxDecoration( 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), borderRadius: BorderRadius.circular(1),
), ),
); );

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

@ -10,6 +10,7 @@ class MessageBubble extends StatelessWidget {
final bool isRecognizing; final bool isRecognizing;
final bool hasImage; final bool hasImage;
final String? imagePath; final String? imagePath;
final Map? mate;
const MessageBubble({ const MessageBubble({
Key? key, Key? key,
@ -19,6 +20,7 @@ class MessageBubble extends StatelessWidget {
this.isRecognizing = false, this.isRecognizing = false,
this.hasImage = false, this.hasImage = false,
this.imagePath, this.imagePath,
this.mate,
}) : super(key: key); }) : super(key: key);
@override @override
@ -28,22 +30,32 @@ class MessageBubble extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Row( child: Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, mainAxisAlignment:
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!isUser) _buildAvatar(isUser, context), if (!isUser) _buildAvatar(isUser, context),
const SizedBox(width: 8), const SizedBox(width: 8),
Flexible( Flexible(
child: Column( child: Column(
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, crossAxisAlignment:
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
Container( 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( decoration: BoxDecoration(
color: _getBubbleColor(context), color: _getBubbleColor(context),
borderRadius: BorderRadius.circular(18).copyWith( borderRadius: BorderRadius.circular(18).copyWith(
bottomLeft: isUser ? const Radius.circular(18) : const Radius.circular(4), bottomLeft: isUser
bottomRight: !isUser ? const Radius.circular(18) : const Radius.circular(4), ? const Radius.circular(18)
: const Radius.circular(4),
bottomRight: !isUser
? const Radius.circular(18)
: const Radius.circular(4),
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@ -179,14 +191,30 @@ class MessageBubble extends StatelessWidget {
} }
// 判断消息内容,显示特殊卡片 // 判断消息内容,显示特殊卡片
if (!isUser) { if (!isUser && mate != null) {
if (message.contains('播放列表') || message.contains('音乐')) { 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); return _buildMusicContent(context);
} else if (message.contains('景点') || message.contains('旅游')) { } else if (mate!.keys.contains('card_weather')) {
return _buildTravelContent(context); //天气
} else if (message.contains('天气') || message.contains('气温') || message.contains('预报')) {
return _buildWeatherContent(context); 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( return Text(
@ -274,11 +302,16 @@ class MessageBubble extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
// 歌曲示例 // 歌曲示例
_buildMusicItem('雨声与钢琴曲', '纯音乐', context), _buildMusicItem(
context,
'雨声与钢琴曲',
"",
'纯音乐',
),
Divider(height: 1, thickness: 1, color: Colors.grey[100]), 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]), Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem('雨天的爵士咖啡馆', '爵士合集', context), _buildMusicItem(context, '雨天的爵士咖啡馆', "", '爵士合集'),
const SizedBox(height: 8), const SizedBox(height: 8),
Center( Center(
@ -313,7 +346,8 @@ class MessageBubble extends StatelessWidget {
label: Text('播放全部'), label: Text('播放全部'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor, 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), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@ -437,7 +471,8 @@ class MessageBubble extends StatelessWidget {
label: Text('查看地图'), label: Text('查看地图'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor, 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), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), 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( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Row( child: Row(
@ -494,7 +530,7 @@ class MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
title, name,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -504,7 +540,7 @@ class MessageBubble extends StatelessWidget {
), ),
SizedBox(height: 2), SizedBox(height: 2),
Text( Text(
artist, singer,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Colors.grey[600],
@ -516,7 +552,8 @@ class MessageBubble extends StatelessWidget {
), ),
IconButton( IconButton(
onPressed: () {}, onPressed: () {},
icon: Icon(Icons.play_circle, color: Theme.of(context).primaryColor), icon:
Icon(Icons.play_circle, color: Theme.of(context).primaryColor),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: BoxConstraints(), constraints: BoxConstraints(),
iconSize: 24, iconSize: 24,
@ -527,7 +564,8 @@ class MessageBubble extends StatelessWidget {
} }
/// 构建地点项目 /// 构建地点项目
Widget _buildLocationItem(String name, String description, BuildContext context) { Widget _buildLocationItem(
String name, String description, BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Row( child: Row(
@ -749,7 +787,8 @@ class MessageBubble extends StatelessWidget {
label: Text('更多地区'), label: Text('更多地区'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor, 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), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@ -822,7 +861,8 @@ class _AnimatedDot extends StatefulWidget {
_AnimatedDotState createState() => _AnimatedDotState(); _AnimatedDotState createState() => _AnimatedDotState();
} }
class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderStateMixin { class _AnimatedDotState extends State<_AnimatedDot>
with SingleTickerProviderStateMixin {
late AnimationController _controller; late AnimationController _controller;
late Animation<double> _animation; late Animation<double> _animation;
@ -842,8 +882,8 @@ class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderSta
reverseCurve: Interval(0.5, 1.0, curve: Curves.easeIn), reverseCurve: Interval(0.5, 1.0, curve: Curves.easeIn),
), ),
)..addListener(() { )..addListener(() {
setState(() {}); setState(() {});
}); });
Future.delayed(Duration(milliseconds: widget.delayMilliseconds), () { Future.delayed(Duration(milliseconds: widget.delayMilliseconds), () {
if (mounted) { if (mounted) {

2
local_plugins/agent_service/pubspec.yaml

@ -19,6 +19,8 @@ dependencies:
path: ../deepvoice_utils path: ../deepvoice_utils
chat_storage: chat_storage:
path: ../chat_storage path: ../chat_storage
music_service:
path: ../music_service
get: ^4.6.5 get: ^4.6.5
dev_dependencies: 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 path: local_plugins/classic_bluetooth
agent_service: agent_service:
path: local_plugins/agent_service path: local_plugins/agent_service
music_service:
path: local_plugins/music_service
dev_dependencies: dev_dependencies:

Loading…
Cancel
Save