Browse Source

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

weicu
liwei1dao 1 year ago
parent
commit
9744ca8c6d
  1. 106
      lib/data/services/ble_manager.dart
  2. 13
      lib/data/services/bluetooth_manager.dart
  3. 10
      lib/data/services/music_manager.dart
  4. 54
      lib/modules/agent/controllers/agent_controller.dart
  5. 140
      lib/modules/agent/views/circular_progress_button.dart
  6. 43
      lib/modules/agent/views/music_card.dart
  7. 244
      lib/modules/home/controllers/home_controller.dart
  8. 223
      lib/modules/home/views/home_view.dart
  9. 11
      lib/modules/music/bindings/music_playlist_binding.dart
  10. 19
      lib/modules/music/controllers/music_playlist_controller.dart
  11. 203
      lib/modules/music/views/music_playlist_view.dart
  12. 1158
      lib/modules/pairing/controllers/pairing_controller.dart
  13. 192
      lib/modules/pairing/views/pairing_view.dart
  14. 258
      lib/modules/pairing/views/pairing_view_new.dart
  15. 9
      lib/routes/app_pages.dart
  16. 3
      lib/routes/app_routes.dart
  17. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  18. 2
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  19. 53
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  20. 13
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  21. 3
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt
  22. 30
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
  23. 14
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift
  24. 15
      local_plugins/music_service/lib/music_service.dart
  25. 2
      test.json

106
lib/data/services/ble_manager.dart

@ -6,6 +6,7 @@ import 'package:deep_voice/core/utils/logger.dart';
import 'package:get_storage/get_storage.dart';
import 'dart:convert';
import 'package:agent_service/agent_service.dart';
import 'bluetooth_manager.dart';
// 重新导出BLE连接状态常量,让外部可以通过BleManager访问
export 'package:ble_service/ble_service.dart' show BleConnectionState;
@ -107,6 +108,10 @@ class BleManager extends GetxService {
final BleService _bleService = BleService();
// 经典蓝牙实例
// 当前连接的设备信息
String? _currentDeviceAddress;
String? _currentDeviceName;
// BLE连接状态
final _connectionState = BleConnectionState.disconnected.obs;
int get connectionState => _connectionState.value;
@ -179,19 +184,8 @@ class BleManager extends GetxService {
state == BleConnectionState.error) {
Logger.info('BLE设备已断开或发生错误');
_isConnected.value = false;
// 更新设备信息
_updateDeviceInfoIfNeeded(
batteryLevel: 0,
leftEarBattery: 0,
rightEarBattery: 0,
caseBattery: 0,
leftEarCharging: false,
rightEarCharging: false,
caseCharging: false,
firmwareVersion: '',
firmwareProductId: '',
firmwareProductColor: '',
);
// 断开连接时不清除设备信息,保留最后的电量信息
// 只有在需要完全清除设备时才重置信息
}
}
@ -408,11 +402,14 @@ class BleManager extends GetxService {
else if (deviceInfo.value == null &&
_isConnected.value &&
(firmwareVersion != null || batteryLevel != null)) {
// 提示尝试创建设备信息但缺少必要信息
Logger.warning('尝试更新设备信息,但设备基本信息不存在');
// 使用保存的设备信息创建新的DeviceInfo对象
String deviceAddress = _currentDeviceAddress ?? "";
String deviceName = _currentDeviceName ?? "";
Logger.info('创建新的设备信息对象: address=$deviceAddress, name=$deviceName');
deviceInfo.value = DeviceInfo(
address: "", // 应确保从连接信息中获取真实值
name: "",
address: deviceAddress,
name: deviceName,
batteryLevel: batteryLevel ?? 0,
leftEarBattery: leftEarBattery ?? 0,
rightEarBattery: rightEarBattery ?? 0,
@ -427,35 +424,59 @@ class BleManager extends GetxService {
}
}
/// 清除设备信息(用于完全断开连接或切换设备时)
void clearDeviceInfo() {
Logger.info('清除设备信息');
deviceInfo.value = null;
_currentDeviceAddress = null;
_currentDeviceName = null;
}
/// 连接成功后获取设备信息
Future<void> fetchDeviceInfo() async {
Logger.info('开始获取BLE设备详细信息');
try {
// 获取电池信息
final batteryResult = await _bleService.getBatteryInfo();
if (batteryResult) {
Logger.info('电池信息获取指令已发送');
} else {
// 确保设备信息对象存在
if (deviceInfo.value == null && _isConnected.value) {
// 创建初始设备信息对象
String deviceAddress = _currentDeviceAddress ?? "";
String deviceName = _currentDeviceName ?? "";
Logger.info('初始化设备信息对象: address=$deviceAddress, name=$deviceName');
deviceInfo.value = DeviceInfo(
address: deviceAddress,
name: deviceName,
batteryLevel: 0,
leftEarBattery: 0,
rightEarBattery: 0,
caseBattery: 0,
leftEarCharging: false,
rightEarCharging: false,
caseCharging: false,
firmwareVersion: '',
firmwareProductId: '',
firmwareProductColor: '',
);
}
// 并行获取所有设备信息,不使用延迟
final results = await Future.wait([
_bleService.getBatteryInfo(),
_bleService.getDeviceVersion(),
_bleService.getProductId(),
]);
Logger.info('设备信息获取结果: 电池=${results[0]}, 版本=${results[1]}, 产品ID=${results[2]}');
if (!results[0]) {
Logger.warning('电池信息获取指令发送失败');
}
// 添加延迟
await Future.delayed(const Duration(seconds: 1));
// 获取设备版本信息
final versionResult = await _bleService.getDeviceVersion();
if (versionResult) {
Logger.info('版本信息获取指令已发送');
} else {
if (!results[1]) {
Logger.warning('版本信息获取指令发送失败');
}
// 添加延迟
await Future.delayed(const Duration(seconds: 1));
// 获取产品id
final productIdResult = await _bleService.getProductId();
if (productIdResult) {
Logger.info('产品id获取指令已发送');
} else {
Logger.warning('产品id获取指令发送失败');
if (!results[2]) {
Logger.warning('产品ID获取指令发送失败');
}
} catch (e) {
Logger.error('获取BLE设备信息失败: ${e.toString()}');
@ -579,6 +600,8 @@ class BleManager extends GetxService {
Logger.info('BLE服务停止成功');
_isConnected.value = false;
_connectionState.value = BleConnectionState.disconnected;
// 停止服务时清除设备信息
clearDeviceInfo();
} else {
Logger.error('BLE服务停止失败');
}
@ -759,6 +782,15 @@ class BleManager extends GetxService {
/// 注册后台扫描
Future<bool> registerBackgroundScan({String? macAddress}) async {
try {
// 保存设备地址信息
if (macAddress != null) {
_currentDeviceAddress = macAddress;
// 从蓝牙管理器获取设备名称
final bluetoothManager = Get.find<BluetoothManager>();
_currentDeviceName = bluetoothManager.currentDevice?.name ?? "";
Logger.info('保存设备信息: address=$_currentDeviceAddress, name=$_currentDeviceName');
}
return await _bleService.registerBackgroundScan(macAddress: macAddress);
} catch (e) {
Logger.error('注册后台扫描失败: ${e.toString()}');

13
lib/data/services/bluetooth_manager.dart

@ -13,6 +13,7 @@ class BluetoothManager extends GetxService {
// 蓝牙状态
final _bluetoothState = 'unknown'.obs;
String get bluetoothState => _bluetoothState.value;
RxString get bluetoothStateRx => _bluetoothState;
// 当前经典蓝牙设备
final Rx<BluetoothDevice?> currentDeviceRx = Rx<BluetoothDevice?>(null);
@ -28,9 +29,19 @@ class BluetoothManager extends GetxService {
super.onInit();
Logger.info('蓝牙服务初始化');
// 设置蓝牙事件监听
_setupBluetoothListener();
// 主动检查当前已连接的设备和蓝牙状态
Future.delayed(Duration.zero, () async {
Logger.info('检查初始蓝牙连接状态');
// 先检查蓝牙是否开启
await isBluetoothEnabled();
// 然后刷新设备连接
await refreshDeviceConnection();
});
}
/// 释放资源

10
lib/data/services/music_manager.dart

@ -70,4 +70,14 @@ class MusicManager extends GetxService {
Future<Map<String, dynamic>> getStatus() async {
return await _musicService.getStatus();
}
//获取播放进度
Future<Map<String, dynamic>> getProgress() async {
return await _musicService.getProgress();
}
//跳转到指定位置
Future<void> seekTo(int positionMs) async {
await _musicService.seekTo(positionMs);
}
}

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

@ -37,6 +37,11 @@ class AgentController extends GetxController {
var isPlaying = false.obs;
var currentUrl = "".obs;
// 播放进度相关状态
var currentPosition = 0.obs; // 当前播放位置(毫秒)
var duration = 0.obs; // 总时长(毫秒)
var progress = 0.0.obs; // 播放进度(0.0-1.0)
// 各种状态标志
final isInitialized = false.obs;
final isListening = false.obs; //是否正在语音识别
@ -64,6 +69,9 @@ class AgentController extends GetxController {
// 导航流事件订阅
StreamSubscription<AmapServiceEvent>? _amapeventSubscription;
// 播放进度更新定时器
Timer? _progressTimer;
// 用于跟踪AI回复状态
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
@ -91,6 +99,8 @@ class AgentController extends GetxController {
textController.dispose();
scrollController.dispose();
_eventSubscription?.cancel();
_amapeventSubscription?.cancel();
_progressTimer?.cancel();
super.onClose();
}
@ -845,17 +855,61 @@ class AgentController extends GetxController {
_musiceManager.play(song);
isPlaying.value = true;
currentUrl.value = song['url'] ?? '';
_startProgressTimer();
}
// 播放音乐
void musicPause() {
_musiceManager.pause();
isPlaying.value = false;
_stopProgressTimer();
}
void musicResume() {
_musiceManager.resume();
isPlaying.value = true;
_startProgressTimer();
}
// 启动进度更新定时器
void _startProgressTimer() {
_stopProgressTimer(); // 先停止现有定时器
_progressTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (isPlaying.value) {
updateMusicProgress();
} else {
timer.cancel();
}
});
}
// 停止进度更新定时器
void _stopProgressTimer() {
_progressTimer?.cancel();
_progressTimer = null;
}
// 获取播放进度
Future<void> updateMusicProgress() async {
try {
var progressInfo = await _musiceManager.getProgress();
currentPosition.value = progressInfo['currentPosition'] ?? 0;
duration.value = progressInfo['duration'] ?? 0;
progress.value = progressInfo['progress'] ?? 0.0;
} catch (e) {
Logger.e('AgentController', '获取播放进度失败: $e');
}
}
// 跳转到指定位置
Future<void> seekToPosition(int positionMs) async {
try {
await _musiceManager.seekTo(positionMs);
// 立即更新进度状态
await updateMusicProgress();
} catch (e) {
Logger.e('AgentController', '跳转播放位置失败: $e');
}
}
// 修改自动停止处理,遵循TTS设置

140
lib/modules/agent/views/circular_progress_button.dart

@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
/// 环形进度条播放按钮组件
class CircularProgressButton extends StatelessWidget {
final bool isPlaying;
final double progress; // 0.0 - 1.0
final VoidCallback onPlayPause;
final Function(double)? onSeek; // 点击进度条跳转回调
final double size;
final Color primaryColor;
final Color backgroundColor;
final double strokeWidth;
const CircularProgressButton({
Key? key,
required this.isPlaying,
required this.progress,
required this.onPlayPause,
this.onSeek,
this.size = 36.0,
required this.primaryColor,
this.backgroundColor = Colors.grey,
this.strokeWidth = 2.0,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (details) {
if (onSeek != null) {
// 计算点击位置对应的进度
final center = Offset(size / 2, size / 2);
final tapPosition = details.localPosition;
final dx = tapPosition.dx - center.dx;
final dy = tapPosition.dy - center.dy;
// 计算角度 (从顶部开始,顺时针)
double angle = math.atan2(dy, dx) + math.pi / 2;
if (angle < 0) angle += 2 * math.pi;
// 转换为进度值 (0.0 - 1.0)
final seekProgress = angle / (2 * math.pi);
// 检查点击是否在圆环区域内
final distance = math.sqrt(dx * dx + dy * dy);
final innerRadius = (size / 2) - strokeWidth - 4;
final outerRadius = (size / 2) + 4;
if (distance >= innerRadius && distance <= outerRadius) {
onSeek!(seekProgress);
} else if (distance < innerRadius) {
// 点击中心区域,播放/暂停
onPlayPause();
}
} else {
onPlayPause();
}
},
child: SizedBox(
width: size,
height: size,
child: Stack(
alignment: Alignment.center,
children: [
// 背景圆环
CustomPaint(
size: Size(size, size),
painter: _CircularProgressPainter(
progress: 1.0,
color: backgroundColor.withValues(alpha: 0.3),
strokeWidth: strokeWidth,
),
),
// 进度圆环
CustomPaint(
size: Size(size, size),
painter: _CircularProgressPainter(
progress: progress,
color: primaryColor,
strokeWidth: strokeWidth,
),
),
// 播放/暂停按钮
Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
color: primaryColor,
size: size * 0.5,
),
],
),
),
);
}
}
/// 环形进度条绘制器
class _CircularProgressPainter extends CustomPainter {
final double progress;
final Color color;
final double strokeWidth;
_CircularProgressPainter({
required this.progress,
required this.color,
required this.strokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width / 2) - strokeWidth / 2;
final paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
// 绘制圆弧,从顶部开始 (-π/2),顺时针绘制
const startAngle = -math.pi / 2;
final sweepAngle = 2 * math.pi * progress;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate is! _CircularProgressPainter ||
oldDelegate.progress != progress ||
oldDelegate.color != color ||
oldDelegate.strokeWidth != strokeWidth;
}
}

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

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
import 'circular_progress_button.dart';
/// 构建音乐项目
Widget buildMusicItem(BuildContext context, int id, String name, String url,
@ -23,7 +24,7 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.03),
color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
@ -83,8 +84,18 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
final isThisPlaying =
controller.isPlaying.value && controller.currentUrl.value == url;
return IconButton(
onPressed: () {
// 只有当前播放的歌曲才显示进度
final currentProgress =
isThisPlaying ? controller.progress.value : 0.0;
return CircularProgressButton(
isPlaying: isThisPlaying,
progress: currentProgress,
primaryColor: Theme.of(context).primaryColor,
backgroundColor: Colors.grey,
size: 36.0,
strokeWidth: 2.0,
onPlayPause: () {
if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) {
controller.musicPause();
@ -99,13 +110,14 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
}
}
},
icon: Icon(
isThisPlaying ? Icons.pause_circle : Icons.play_circle,
color: Theme.of(context).primaryColor,
),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 36,
onSeek: isThisPlaying
? (seekProgress) {
// 只有当前播放的歌曲才允许跳转
final targetPosition =
(controller.duration.value * seekProgress).round();
controller.seekToPosition(targetPosition);
}
: null,
);
}),
],
@ -124,7 +136,8 @@ Widget buildMusicContent(
Text(
'以下是一些推荐的音乐:',
style: TextStyle(
color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87,
color:
isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
@ -198,10 +211,14 @@ Widget buildMusicContent(
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {},
onPressed: () {
// 跳转到完整歌单页面,传递完整的歌单数据
Get.toNamed('/music/playlist', arguments: playlist);
},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(

244
lib/modules/home/controllers/home_controller.dart

@ -6,32 +6,26 @@ import 'package:flutter/material.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
/// 连接状态枚举
enum ConnectionStatus {
disconnected, // 未连接
connecting, // 连接中
connected, // 已连接
failed // 连接失败
disconnected,
connecting,
connected,
failed
}
/// 主页控制器,管理设备连接状态和信息展示
class HomeController extends GetxController {
static const String _tag = 'HomeCtrl';
static HomeController get to => Get.find();
// 连接状态相关
final _connectionStatus = ConnectionStatus.disconnected.obs;
final _connectionErrorMsg = ''.obs;
final _bluetoothState = 'unknown'.obs;
// Getter方法
ConnectionStatus get connectionStatus => _connectionStatus.value;
String get connectionErrorMsg => _connectionErrorMsg.value;
String get bluetoothState => _bluetoothState.value;
final _isFullyConnected = false.obs;
String? _lastConnectedDeviceName;
bool get isFullyConnected => _isFullyConnected.value;
RxBool get isFullyConnectedRx => _isFullyConnected;
BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice;
Rx<DeviceInfo?> get deviceInfo => _bleManager.deviceInfo;
bool get isConnected => currentDevice != null;
// 设备详细信息的getter,从deviceInfo获取数据
final leftEarBattery = 0.obs;
final rightEarBattery = 0.obs;
final caseBattery = 0.obs;
@ -39,15 +33,6 @@ class HomeController extends GetxController {
final rightEarCharging = false.obs;
final caseCharging = false.obs;
final firmwareVersion = ''.obs;
// int get leftEarBattery => deviceInfo?.leftEarBattery ?? 0;
// int get rightEarBattery => deviceInfo?.rightEarBattery ?? 0;
// int get caseBattery => deviceInfo?.caseBattery ?? 0;
// bool get leftEarCharging => deviceInfo?.leftEarCharging ?? false;
// bool get rightEarCharging => deviceInfo?.rightEarCharging ?? false;
// bool get caseCharging => deviceInfo?.caseCharging ?? false;
// String get firmwareVersion => deviceInfo?.firmwareVersion ?? '';
/// 当有新的deviceInfo时更新所有Observable变量
void updateDeviceInfo(DeviceInfo? newDeviceInfo) {
leftEarBattery.value = newDeviceInfo?.leftEarBattery ?? 0;
rightEarBattery.value = newDeviceInfo?.rightEarBattery ?? 0;
@ -58,12 +43,10 @@ class HomeController extends GetxController {
firmwareVersion.value = newDeviceInfo?.firmwareVersion ?? '';
}
// 监听和订阅
Worker? _deviceInfoWorker;
Worker? _bluetoothStateWorker;
Worker? _deviceWorker;
Worker? _classicBluetoothWorker;
Worker? _bleConnectionWorker;
// 蓝牙服务
final _bluetoothManager = Get.find<BluetoothManager>();
final _bleManager = Get.find<BleManager>();
@override
@ -75,175 +58,120 @@ class HomeController extends GetxController {
@override
void onClose() {
_deviceInfoWorker?.dispose();
_bluetoothStateWorker?.dispose();
_deviceWorker?.dispose();
_classicBluetoothWorker?.dispose();
_bleConnectionWorker?.dispose();
super.onClose();
}
/// 使用缓存的蓝牙设备信息初始化
void _initWithCachedInfo() {
// 使用蓝牙服务中的当前状态
_bluetoothState.value = _bluetoothManager.bluetoothState;
// 根据当前设备状态设置连接状态
if (currentDevice != null) {
_connectionStatus.value = ConnectionStatus.connected;
_connectionErrorMsg.value = '';
updateDeviceInfo(_bleManager.deviceInfo.value);
Logger.info('主页加载缓存设备信息: ${currentDevice!.name}');
} else {
_connectionStatus.value = ConnectionStatus.disconnected;
if (_bluetoothState.value == 'off') {
_connectionErrorMsg.value = '蓝牙未开启';
} else {
_connectionErrorMsg.value = '未连接到设备';
}
}
// 设置监听器监听蓝牙服务状态变化
_setupStateListeners();
// 在初始化时自动触发一次刷新设备连接状态
refreshDeviceConnection();
_checkConnectionStatus();
}
/// 设置状态监听器
void _setupStateListeners() {
// 设备信息监听 - 设备信息更新时自动触发UI更新
_deviceInfoWorker = ever(_bleManager.deviceInfo, (DeviceInfo? info) {
Logger.info('设备信息监听');
if (info != null) {
Logger.info('主页更新设备信息: ${info.toString()}');
updateDeviceInfo(info);
} else {
Logger.info('主页清空设备信息');
}
update();
});
// 蓝牙状态监听
_bluetoothStateWorker =
ever(_bluetoothManager.bluetoothState.obs, (String state) {
_bluetoothState.value = state;
// 如果蓝牙关闭,更新错误信息
if (state == 'off') {
_connectionErrorMsg.value = '蓝牙未开启';
_connectionStatus.value = ConnectionStatus.disconnected;
}
update();
});
// 设备连接状态监听 - 直接监听设备变化
_deviceWorker =
ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) {
_classicBluetoothWorker = ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) {
if (device != null) {
// 设备已连接
if (_connectionStatus.value != ConnectionStatus.connected) {
_connectionStatus.value = ConnectionStatus.connected;
_connectionErrorMsg.value = '';
Logger.info('主页检测到设备已连接: ${device.name}');
}
} else if (_connectionStatus.value == ConnectionStatus.connected) {
// 设备已断开
_connectionStatus.value = ConnectionStatus.disconnected;
_connectionErrorMsg.value = '设备已断开连接';
Logger.info('主页检测到设备已断开连接');
_lastConnectedDeviceName = device.name;
}
update();
_checkConnectionStatus();
});
_bleConnectionWorker = ever(_bleManager.connectionStateRx, (int state) {
_checkConnectionStatus();
});
}
/// 刷新设备连接状态
/// 注意:此方法不会主动刷新设备,只更新UI状态
Future<void> refreshDeviceConnection() async {
// 更新当前状态以反映UI变化
bool hasDevice = currentDevice != null;
if (hasDevice) {
_connectionStatus.value = ConnectionStatus.connected;
_connectionErrorMsg.value = '';
await _bleManager.fetchDeviceInfo();
Logger.info('使用缓存设备信息: ${currentDevice!.name}');
} else {
if (_bluetoothState.value == 'off') {
_connectionErrorMsg.value = '蓝牙未开启';
} else {
_connectionErrorMsg.value = '未连接到设备';
}
_connectionStatus.value = ConnectionStatus.disconnected;
}
// 刷新UI
update();
await _checkConnectionStatus();
}
// 导航和功能方法
/// 重新扫描设备
void rescanDevices() {
Get.toNamed(Routes.pairing);
}
/// 打开翻译功能
void openTranslation() {
Get.toNamed(Routes.translation);
}
/// 展示AI功能
void exploreAIFeatures() {
Get.toNamed(Routes.agent);
}
Future<void> _checkConnectionStatus() async {
try {
final currentBleState = await _bleManager.getConnectionState();
bool hasClassicBluetooth = currentDevice != null;
bool hasBleConnection = currentBleState == 2;
bool wasConnected = _isFullyConnected.value;
bool isNowConnected = hasClassicBluetooth && hasBleConnection;
if (isNowConnected && currentDevice == null) {
isNowConnected = false;
}
if (_isFullyConnected.value != isNowConnected) {
_isFullyConnected.value = isNowConnected;
_isFullyConnected.refresh();
update();
if (!wasConnected && isNowConnected) {
await _bleManager.fetchDeviceInfo();
}
}
update();
} catch (e) {
Logger.e(_tag, '检查连接状态失败: $e');
if (_isFullyConnected.value) {
_isFullyConnected.value = false;
_isFullyConnected.refresh();
update();
}
}
}
// UI辅助方法
/// 获取连接状态对应的颜色
Color getConnectionStatusColor() {
switch (_connectionStatus.value) {
case ConnectionStatus.connected:
return Colors.green;
case ConnectionStatus.connecting:
return Colors.amber;
case ConnectionStatus.failed:
return Colors.red;
case ConnectionStatus.disconnected:
default:
return Colors.grey;
}
return _isFullyConnected.value ? Colors.green : Colors.grey;
}
/// 获取连接状态对应的文本
String getConnectionStatusText() {
if (bluetoothState == 'off') {
return 'bluetoothOff'.tr;
}
switch (_connectionStatus.value) {
case ConnectionStatus.connected:
return 'connected'.tr;
case ConnectionStatus.connecting:
return 'connecting'.tr;
case ConnectionStatus.failed:
return 'connectionFailure'.tr;
case ConnectionStatus.disconnected:
default:
return 'unconnected'.tr;
}
return _isFullyConnected.value ? 'connected'.tr : 'unconnected'.tr;
}
/// 获取设备名称
bool get shouldShowDeviceInfo => currentDevice != null;
String getDeviceName() {
return currentDevice?.name ?? 'unknownEquipment'.tr;
if (_isFullyConnected.value && currentDevice == null) {
_checkConnectionStatus();
}
final deviceName = currentDevice?.name;
if (deviceName != null && deviceName.isNotEmpty) {
return deviceName;
}
final bleDeviceName = deviceInfo.value?.name;
if (bleDeviceName != null && bleDeviceName.isNotEmpty) {
return bleDeviceName;
}
if (!_isFullyConnected.value && _lastConnectedDeviceName != null && _lastConnectedDeviceName!.isNotEmpty) {
return _lastConnectedDeviceName!;
}
return 'unknownEquipment'.tr;
}
/// 获取信号强度(模拟数据)
String getSignalStrength() {
if (!isConnected) {
if (!_isFullyConnected.value) {
return 'zeroSignal'.tr;
}
@ -258,12 +186,10 @@ class HomeController extends GetxController {
}
}
/// 获取设备ID
String getDeviceId() {
return deviceInfo.value?.address ?? currentDevice?.address ?? 'unknown'.tr;
}
/// 获取设备电量文本
String getBatteryText() {
return deviceInfo.value?.batteryLevel != null &&
deviceInfo.value!.batteryLevel > 0
@ -271,9 +197,8 @@ class HomeController extends GetxController {
: 'unknown'.tr;
}
/// 获取耳机左右电量文本
String getEarbudsText() {
if (deviceInfo == null ||
if (deviceInfo.value == null ||
(deviceInfo.value!.leftEarBattery <= 0 &&
deviceInfo.value!.rightEarBattery <= 0 ||
deviceInfo.value!.caseBattery > 100)) {
@ -295,7 +220,6 @@ class HomeController extends GetxController {
return '左耳: $leftStatus / 右耳: $rightStatus';
}
/// 获取充电仓电量文本
String getCaseText() {
if (deviceInfo.value == null ||
deviceInfo.value!.caseBattery <= 0 ||
@ -311,9 +235,8 @@ class HomeController extends GetxController {
return '充电仓: $caseStatus';
}
/// 获取充电状态文本
String getChargingText() {
if (deviceInfo == null) {
if (deviceInfo.value == null) {
return 'unknown'.tr;
}
@ -324,7 +247,6 @@ class HomeController extends GetxController {
return isCharging ? 'charging'.tr : 'notCharging'.tr;
}
/// 获取设备版本文本
String getVersionText() {
return deviceInfo.value?.firmwareVersion.isNotEmpty == true
? deviceInfo.value!.firmwareVersion

223
lib/modules/home/views/home_view.dart

@ -5,8 +5,6 @@ import '../controllers/home_controller.dart';
import '../../../routes/app_pages.dart';
import '../../../routes/app_routes.dart';
import '../../../core/widgets/common_bottom_nav.dart';
// 导入枚举
import '../controllers/home_controller.dart' show ConnectionStatus;
// 呼吸灯动画组件
class AnimatedPulsing extends StatefulWidget {
@ -205,8 +203,12 @@ class HomeView extends GetView<HomeController> {
// 设备状态卡片
Widget _buildDeviceStatusCard(bool isDarkMode) {
return Container(
decoration: BoxDecoration(
return Obx(() {
// 使用连接状态作为key,强制在状态改变时重建widget
final isConnected = controller.isFullyConnectedRx.value;
return Container(
key: ValueKey('device_status_$isConnected'),
decoration: BoxDecoration(
color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
@ -226,18 +228,9 @@ class HomeView extends GetView<HomeController> {
// 设备信息
SizedBox(height: 12.h),
Obx(() {
// 如果连接失败且有错误信息,显示错误提示
if (controller.connectionStatus == ConnectionStatus.failed &&
controller.connectionErrorMsg.isNotEmpty) {
return _buildConnectionErrorSection(isDarkMode); //错误提示
}
// 如果正在连接中,显示连接中提示
if (controller.connectionStatus == ConnectionStatus.connecting) {
return _buildConnectingIndicator(); //连接中提示
}
return controller.isConnected
// 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断
// 使用响应式访问确保UI更新
return controller.isFullyConnectedRx.value
? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息
: _buildDisconnectedInfo(isDarkMode); //未连接提示
}),
@ -245,7 +238,8 @@ class HomeView extends GetView<HomeController> {
// 连接按钮(仅在未连接时显示)
SizedBox(height: 12.h),
Obx(() {
if (controller.connectionStatus == ConnectionStatus.disconnected) {
// 使用响应式访问确保UI更新
if (!controller.isFullyConnectedRx.value) {
return _buildConnectButton();
} else {
return const SizedBox();
@ -253,7 +247,8 @@ class HomeView extends GetView<HomeController> {
}),
],
),
);
);
});
}
// 设备状态头部
@ -332,95 +327,7 @@ class HomeView extends GetView<HomeController> {
}
// 连接错误部分
Widget _buildConnectionErrorSection(bool isDarkMode) {
return Column(
children: [
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color:
isDarkMode ? Colors.red[900]!.withOpacity(0.2) : Colors.red[50],
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isDarkMode ? Colors.red[800]! : Colors.red[200]!,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.error_outline,
color: Colors.red,
size: 18.sp,
),
SizedBox(width: 8.w),
Expanded(
child: Text(
controller.connectionErrorMsg,
style: TextStyle(
fontSize: 12.sp,
height: 1.4,
color: isDarkMode ? Colors.red[300] : Colors.red[800],
),
),
),
],
),
),
SizedBox(height: 12.h),
// 连接按钮
ElevatedButton(
onPressed: controller.rescanDevices,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 40.h),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.r),
),
),
child: Text(
'reconnect'.tr, // 重新连接
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
// 连接中指示器
Widget _buildConnectingIndicator() {
return Container(
padding: EdgeInsets.symmetric(vertical: 20.h),
child: Center(
child: Column(
children: [
SizedBox(
width: 30.w,
height: 30.w,
child: CircularProgressIndicator(
strokeWidth: 2.5.w,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
),
SizedBox(height: 12.h),
Text(
'connectingDevice'.tr,
style: TextStyle(
fontSize: 13.sp,
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
// 已连接设备信息(新增设备名称 + 信号强度 + 电池信息)
Widget _buildConnectedDeviceInfo(bool isDarkMode) {
@ -608,110 +515,6 @@ class HomeView extends GetView<HomeController> {
}
// 设备详情卡片
Widget _buildDeviceDetailsCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 设备ID
_buildInfoRow(
icon: Icons.perm_device_info,
label: 'deviceId'.tr,
value: controller.getDeviceId(),
isDarkMode: isDarkMode,
),
SizedBox(height: 8.h),
// 固件版本
_buildInfoRow(
icon: Icons.info_outline,
label: 'firmwareVersion'.tr,
value: controller.firmwareVersion.value,
isDarkMode: isDarkMode,
),
// 连接状态标签
// SizedBox(height: 10.h),
// Container(
// padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
// decoration: BoxDecoration(
// color: controller.getConnectionStatusColor().withOpacity(0.1),
// borderRadius: BorderRadius.circular(6.r),
// border: Border.all(
// color: controller.getConnectionStatusColor().withOpacity(0.3),
// width: 1,
// ),
// ),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Icon(
// Icons.check_circle_outline,
// size: 12.sp,
// color: controller.getConnectionStatusColor(),
// ),
// SizedBox(width: 4.w),
// Text(
// controller.getConnectionStatusText(),
// style: TextStyle(
// fontSize: 11.sp,
// color: controller.getConnectionStatusColor(),
// fontWeight: FontWeight.w500,
// ),
// ),
// ],
// ),
// ),
],
),
);
}
// 信息行
Widget _buildInfoRow({
required IconData icon,
required String label,
required String value,
required bool isDarkMode,
}) {
return Row(
children: [
Icon(
icon,
size: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
SizedBox(width: 6.w),
Text(
'$label: ',
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
Expanded(
child: Text(
value.isEmpty ? 'unknown'.tr : value,
style: TextStyle(
fontSize: 11.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
// 未连接信息
Widget _buildDisconnectedInfo(bool isDarkMode) {

11
lib/modules/music/bindings/music_playlist_binding.dart

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/music_playlist_controller.dart';
class MusicPlaylistBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<MusicPlaylistController>(
() => MusicPlaylistController(),
);
}
}

19
lib/modules/music/controllers/music_playlist_controller.dart

@ -0,0 +1,19 @@
import 'package:get/get.dart';
class MusicPlaylistController extends GetxController {
// 歌单数据
final playlist = <Map<String, dynamic>>[].obs;
@override
void onInit() {
super.onInit();
// 从路由参数获取歌单数据
final List<Map<String, dynamic>> playlistData = Get.arguments ?? [];
playlist.assignAll(playlistData);
}
@override
void onClose() {
super.onClose();
}
}

203
lib/modules/music/views/music_playlist_view.dart

@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../agent/controllers/agent_controller.dart';
import '../../agent/views/circular_progress_button.dart';
class MusicPlaylistView extends StatelessWidget {
const MusicPlaylistView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final controller = Get.find<AgentController>();
final List<Map<String, dynamic>> playlist = Get.arguments ?? [];
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: const Text('音乐播放列表'),
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
foregroundColor: isDarkMode ? Colors.white : Colors.black,
elevation: 0,
),
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
body: playlist.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.music_off,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'暂无音乐',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: playlist.length,
itemBuilder: (context, index) {
final item = playlist[index];
final id = item["id"]?.toString() ?? "";
final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? "";
final singer = item["singer"] ?? item["sgener"] ?? "未知歌手";
final image = item["image"] ?? "";
return _buildMusicItem(
context,
controller,
id,
name,
url,
singer,
image,
isDarkMode,
);
},
),
);
}
Widget _buildMusicItem(
BuildContext context,
AgentController controller,
String id,
String name,
String url,
String singer,
String image,
bool isDarkMode,
) {
final song = {
'id': id,
'title': name,
'url': url,
'artist': singer,
'coverUrl': image,
};
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: Row(
children: [
// 封面图片
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey[300],
),
child: image.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
image,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.music_note,
color: Colors.grey[600],
);
},
),
)
: Icon(
Icons.music_note,
color: Colors.grey[600],
),
),
const SizedBox(width: 12),
// 歌曲信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
singer,
style: TextStyle(
fontSize: 14,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 12),
// 播放按钮
Obx(() {
final isThisPlaying =
controller.isPlaying.value && controller.currentUrl.value == url;
// 只有当前播放的歌曲才显示进度
final currentProgress = isThisPlaying ? controller.progress.value : 0.0;
return CircularProgressButton(
isPlaying: isThisPlaying,
progress: currentProgress,
primaryColor: Theme.of(context).primaryColor,
backgroundColor: Colors.grey,
size: 40.0,
strokeWidth: 2.5,
onPlayPause: () {
if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) {
controller.musicPause();
} else {
controller.musicPlay(song);
}
} else {
if (controller.currentUrl.value == url) {
controller.musicResume();
} else {
controller.musicPlay(song);
}
}
},
onSeek: isThisPlaying ? (seekProgress) {
// 只有当前播放的歌曲才允许跳转
final targetPosition = (controller.duration.value * seekProgress).round();
controller.seekToPosition(targetPosition);
} : null,
);
}),
],
),
);
}
}

1158
lib/modules/pairing/controllers/pairing_controller.dart

File diff suppressed because it is too large

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

@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/pairing_controller.dart';
import '../../../core/widgets/common_bottom_nav.dart';
import '../../../core/themes/app_theme.dart';
class PairingView extends GetView<PairingController> {
const PairingView({Key? key}) : super(key: key);
@ -97,28 +95,25 @@ class PairingView extends GetView<PairingController> {
// 设备动画 - 根据状态显示不同的动画效果
Widget _buildDeviceAnimation(bool isDarkMode) {
return Obx(() {
final state = controller.state;
final state = controller.currentState;
final isLoading = controller.isLoading.value;
final isReadyToStart = controller.isReadyToStart.value;
final isManualMode = controller.isManualMode.value;
// 根据状态决定动画颜色
Color primaryColor = Color(0xFF0066CC);
Color secondaryColor = Color(0xFF5E5CE6);
// 错误状态使用红色
if (state.name.startsWith('exit')) {
if (state == PairingState.error) {
primaryColor = Colors.red;
secondaryColor = Colors.redAccent;
}
// 成功状态使用绿色
else if (state == PairingState.paired) {
else if (state == PairingState.completed) {
primaryColor = Colors.green;
secondaryColor = Colors.greenAccent;
}
// 手动模式待机状态的特殊处理
if (isManualMode && isReadyToStart) {
// 待机状态的特殊处理
if (state == PairingState.idle || state == PairingState.waitingClassic) {
return Container(
width: 240.w,
height: 240.w,
@ -237,19 +232,24 @@ class PairingView extends GetView<PairingController> {
// 根据状态获取图标
IconData _getIconForState(PairingState state) {
switch (state) {
case PairingState.paired:
case PairingState.completed:
return Icons.check_circle;
case PairingState.exitNoPermission:
case PairingState.exitBtOff:
return Icons.bluetooth_disabled;
case PairingState.exitNoClassic:
case PairingState.exitNotMatch:
return Icons.headphones_outlined;
case PairingState.exitPairFailed:
case PairingState.exitTimeout:
case PairingState.exitIllegal:
case PairingState.exitBound:
return Icons.error_outline;
case PairingState.associating:
return Icons.link;
case PairingState.error:
final errorType = controller.errorType;
switch (errorType) {
case ErrorType.bluetoothOff:
case ErrorType.noPermission:
return Icons.bluetooth_disabled;
case ErrorType.noClassicDevice:
case ErrorType.deviceNotMatch:
return Icons.headphones_outlined;
case ErrorType.associationFailed:
return Icons.link_off;
default:
return Icons.error_outline;
}
default:
return Icons.headphones;
}
@ -258,8 +258,8 @@ class PairingView extends GetView<PairingController> {
// 状态信息
Widget _buildStatusInfo(bool isDarkMode) {
return Obx(() {
final status = controller.statusDescription.value;
final state = controller.state;
final status = controller.statusText.value;
final state = controller.currentState;
final device = controller.currentDevice;
return Column(
@ -289,10 +289,9 @@ class PairingView extends GetView<PairingController> {
// 当前连接设备(有设备时才显示)
if (device != null &&
(state == PairingState.classicOk ||
state == PairingState.serverVerify ||
state == PairingState.pairing ||
state == PairingState.paired)) ...[
(state == PairingState.verifying ||
state == PairingState.waitingBle ||
state == PairingState.completed)) ...[
SizedBox(height: 24.h),
_buildConnectedDevice(device, isDarkMode),
],
@ -303,37 +302,43 @@ class PairingView extends GetView<PairingController> {
// 根据状态获取副标题
String _getSubtitleForState(PairingState state) {
if (controller.isManualMode.value && controller.isReadyToStart.value) {
return '点击下方按钮开始配对您的DeapSound设备';
}
switch (state) {
case PairingState.init:
case PairingState.btReady:
return controller.isManualMode.value ? '准备开始配对' : '正在准备配对环境...';
case PairingState.classicOk:
case PairingState.idle:
return '准备开始配对';
case PairingState.checking:
return '正在准备配对环境...';
case PairingState.waitingClassic:
return '请先通过系统蓝牙连接DeapSound设备';
case PairingState.serverVerify:
case PairingState.pairing:
case PairingState.verifying:
return '正在验证设备...';
case PairingState.associating:
return '正在建立设备关联...';
case PairingState.waitingBle:
return '请稍候,正在完成配对...';
case PairingState.paired:
case PairingState.completed:
return '配对成功!即将跳转到主页...';
case PairingState.exitNoPermission:
return '需要蓝牙权限才能继续';
case PairingState.exitBtOff:
return '请开启蓝牙后重试';
case PairingState.exitNoClassic:
return '请先通过系统蓝牙连接DeapSound设备';
case PairingState.exitNotMatch:
return '请确保连接的是DeapSound设备';
case PairingState.exitTimeout:
return '扫描超时,请确保设备在附近';
case PairingState.exitPairFailed:
return '配对失败,请重试';
case PairingState.exitBound:
return '该设备已被其他账号绑定';
case PairingState.exitIllegal:
return '设备验证失败,请检查设备是否正常';
case PairingState.error:
final errorType = controller.errorType;
switch (errorType) {
case ErrorType.bluetoothOff:
return '请开启蓝牙后重试';
case ErrorType.noPermission:
return '需要蓝牙权限才能继续';
case ErrorType.noClassicDevice:
return '请先通过系统蓝牙连接DeapSound设备';
case ErrorType.deviceNotMatch:
return '请确保连接的是DeapSound设备';
case ErrorType.deviceBound:
return '该设备已被其他账号绑定';
case ErrorType.associationFailed:
return '设备关联失败,请重试';
case ErrorType.bleTimeout:
return '扫描超时,请确保设备在附近';
case ErrorType.networkError:
return '网络错误,请检查网络连接';
default:
return '配对失败,请重试';
}
default:
return '';
}
@ -411,23 +416,32 @@ class PairingView extends GetView<PairingController> {
);
}
// 获取连接按钮图标
IconData _getConnectButtonIcon() {
final buttonText = controller.connectButtonText.value;
if (buttonText.contains('蓝牙')) {
return Icons.bluetooth;
} else if (buttonText.contains('设置')) {
return Icons.settings;
}
return Icons.bluetooth_searching;
}
// 操作按钮
Widget _buildActionButtons(bool isDarkMode) {
return Obx(() {
final showActionButton = controller.showActionButton;
final canSkip = controller.canSkip;
final actionButtonText = controller.actionButtonText.value;
final isReadyToStart = controller.isReadyToStart.value;
final isManualMode = controller.isManualMode.value;
final showConnectButton = controller.showConnectButton.value;
final showRetryButton = controller.showRetryButton.value;
final showSkipButton = controller.showSkipButton.value;
return Container(
width: double.infinity,
child: Column(
children: [
// 手动模式:显示开始配对按钮
if (isManualMode && isReadyToStart)
// 显示连接按钮
if (showConnectButton)
ElevatedButton(
onPressed: () => controller.startManualPairing(),
onPressed: () => controller.onConnectTap(),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF0066CC),
foregroundColor: Colors.white,
@ -441,10 +455,10 @@ class PairingView extends GetView<PairingController> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bluetooth_searching, size: 20.w),
Icon(_getConnectButtonIcon(), size: 20.w),
SizedBox(width: 8.w),
Text(
'开始配对设备',
controller.connectButtonText.value,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -454,13 +468,12 @@ class PairingView extends GetView<PairingController> {
),
),
// 原有的状态按钮(错误处理等)
if (showActionButton && !isManualMode)
// 显示重试按钮
if (showRetryButton)
ElevatedButton(
onPressed: () => controller.handleActionButtonTap(),
onPressed: () => controller.onRetryTap(),
style: ElevatedButton.styleFrom(
backgroundColor:
_getButtonColor(controller.state, isDarkMode),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
@ -470,7 +483,7 @@ class PairingView extends GetView<PairingController> {
minimumSize: Size(double.infinity, 56.h),
),
child: Text(
actionButtonText,
controller.retryButtonText.value,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -478,11 +491,11 @@ class PairingView extends GetView<PairingController> {
),
),
// 跳过按钮和重置按钮
if (canSkip) ...[
// 跳过按钮
if (showSkipButton) ...[
SizedBox(height: 12.h),
TextButton(
onPressed: () => controller.skipPairing(),
onPressed: () => controller.onSkipTap(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
@ -498,41 +511,10 @@ class PairingView extends GetView<PairingController> {
),
],
// 添加重置按钮(在错误状态下)
if (!isManualMode &&
!controller.isLoading.value &&
controller.state.name.startsWith('exit')) ...[
SizedBox(height: 8.h),
TextButton(
onPressed: () => controller.resetToManualMode(),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: EdgeInsets.symmetric(vertical: 8.h),
),
child: Text(
'重新配对',
style: TextStyle(fontSize: 14.sp),
),
),
],
],
),
);
});
}
// 根据状态获取按钮颜色
Color _getButtonColor(PairingState state, bool isDarkMode) {
switch (state) {
case PairingState.exitNoPermission:
case PairingState.exitBtOff:
return isDarkMode ? Colors.grey[800]! : Colors.grey[600]!;
case PairingState.exitPairFailed:
case PairingState.exitBound:
case PairingState.exitIllegal:
return Colors.red;
default:
return Color(0xFF0066CC);
}
}
}

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

@ -0,0 +1,258 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/pairing_controller.dart';
import '../../../core/themes/app_theme.dart';
/// 配对视图 - 简化版本
class PairingView extends GetView<PairingController> {
const PairingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(24.w),
child: Column(
children: [
// 顶部标题
_buildHeader(isDarkMode),
// 中间内容
Expanded(
child: Center(
child: Obx(() => _buildContent(isDarkMode)),
),
),
// 底部按钮
Obx(() => _buildButtons(isDarkMode)),
SizedBox(height: 20.h),
],
),
),
),
);
}
/// 构建标题
Widget _buildHeader(bool isDarkMode) {
return Column(
children: [
Text(
'设备配对',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 8.h),
Text(
'连接您的 DeepSound 耳机',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
SizedBox(height: 40.h),
],
);
}
/// 构建主要内容
Widget _buildContent(bool isDarkMode) {
final state = controller.currentState;
final isLoading = controller.isLoading.value;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 状态图标
_buildStateIcon(state, isLoading, isDarkMode),
SizedBox(height: 40.h),
// 状态文本
Text(
controller.statusText.value,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
textAlign: TextAlign.center,
),
// 错误详情
if (state == PairingState.error) ...[
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.red.withOpacity(0.3)),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Colors.red,
size: 20.sp,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
controller.errorMessage,
style: TextStyle(
fontSize: 14.sp,
color: Colors.red,
),
),
),
],
),
),
],
],
);
}
/// 构建状态图标
Widget _buildStateIcon(PairingState state, bool isLoading, bool isDarkMode) {
if (isLoading) {
return CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(Get.context!).primaryColor,
),
);
}
IconData iconData;
Color iconColor;
switch (state) {
case PairingState.idle:
case PairingState.checking:
iconData = Icons.bluetooth_searching;
iconColor = Colors.blue;
break;
case PairingState.waitingClassic:
iconData = Icons.bluetooth;
iconColor = Colors.grey;
break;
case PairingState.verifying:
iconData = Icons.verified_user;
iconColor = Colors.orange;
break;
case PairingState.associating:
iconData = Icons.link;
iconColor = Colors.blue;
break;
case PairingState.waitingBle:
iconData = Icons.sync;
iconColor = Colors.blue;
break;
case PairingState.completed:
iconData = Icons.check_circle;
iconColor = Colors.green;
break;
case PairingState.error:
iconData = Icons.error;
iconColor = Colors.red;
break;
}
return Container(
width: 100.w,
height: 100.w,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
iconData,
size: 60.sp,
color: iconColor,
),
);
}
/// 构建按钮
Widget _buildButtons(bool isDarkMode) {
final showConnectButton = controller.showConnectButton.value;
final showRetryButton = controller.showRetryButton.value;
final showSkipButton = controller.showSkipButton.value;
return Column(
children: [
// 主要操作按钮
if (showConnectButton)
_buildPrimaryButton(
'连接设备',
controller.onConnectTap,
isDarkMode,
),
if (showRetryButton)
_buildPrimaryButton(
'重试',
controller.onRetryTap,
isDarkMode,
),
SizedBox(height: 12.h),
// 跳过按钮
if (showSkipButton)
TextButton(
onPressed: controller.onSkipTap,
child: Text(
'跳过',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
],
);
}
/// 构建主要按钮
Widget _buildPrimaryButton(String text, VoidCallback onPressed, bool isDarkMode) {
return SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(Get.context!).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
elevation: 0,
),
child: Text(
text,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
);
}
}

9
lib/routes/app_pages.dart

@ -42,6 +42,8 @@ import '../modules/ota_upgrade/views/ota_upgrade_view.dart';
import '../modules/ota_upgrade/bindings/ota_upgrade_binding.dart';
import '../modules/realtime/bindings/realtime_binding.dart';
import '../modules/realtime/views/realtime_view.dart';
import '../modules/music/views/music_playlist_view.dart';
import '../modules/music/bindings/music_playlist_binding.dart';
abstract class AppPages {
static final pages = [
@ -206,5 +208,12 @@ abstract class AppPages {
page: () => const RealtimeView(),
binding: RealtimeBinding(),
),
GetPage(
name: Routes.musicPlaylist,
page: () => const MusicPlaylistView(),
binding: MusicPlaylistBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
];
}

3
lib/routes/app_routes.dart

@ -46,6 +46,9 @@ abstract class Routes {
static const agent = '/agent'; // 在线客服
static const navigation = '/navigation'; // 实时导航
// 音乐相关
static const musicPlaylist = '/music/playlist'; // 音乐播放列表
// 实时语音对话
static const realtime = '/realtime';
}

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

@ -631,7 +631,7 @@ object AgentService : CoroutineScope {
)
val _systemPrompt = Utils.fillTemplate(systemPrompt,location)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
// Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
}
// 再添加历史消息

2
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt

@ -391,7 +391,7 @@ class BleCompanionManager(private val context: Context) {
Log.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器")
return
}
unregisterBackgroundScan()
// 构建过滤器
val filter = ScanFilter.Builder()
.setDeviceAddress(mac)

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

@ -43,6 +43,39 @@ class MusicService : MediaBrowserServiceCompat() {
fun isServiceRunning() = isRunning.get()
fun isPlaying() = currentStatus
fun getCurrentUrl(): String? = currentUrlInternal
// 获取播放进度信息
fun getProgress(): Map<String, Any> {
return if (instance != null && instance!!.mediaPlayer != null) {
val player = instance!!.mediaPlayer!!
try {
val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0
val duration = player.duration
val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0
mapOf(
"currentPosition" to currentPosition,
"duration" to duration,
"progress" to progress
)
} catch (e: Exception) {
Log.e(TAG, "获取播放进度失败: ${e.message}")
mapOf(
"currentPosition" to 0,
"duration" to 0,
"progress" to 0.0
)
}
} else {
mapOf(
"currentPosition" to 0,
"duration" to 0,
"progress" to 0.0
)
}
}
private var instance: MusicService? = null
}
private var isReceiverRegistered = false
private lateinit var mediaSession: MediaSessionCompat
@ -51,6 +84,7 @@ class MusicService : MediaBrowserServiceCompat() {
override fun onCreate() {
super.onCreate()
instance = this
isRunning.set(true)
setupMediaSession()
startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound",""))
@ -116,6 +150,12 @@ class MusicService : MediaBrowserServiceCompat() {
"stop" -> stopSelf()
"next" -> if (currentIndex + 1 < playlist.size) play(playlist[++currentIndex])
"previous" -> if (currentIndex - 1 >= 0) play(playlist[--currentIndex])
"seekTo" -> {
val position = intent.getIntExtra("position", -1)
if (position >= 0) {
seekTo(position)
}
}
}
return START_STICKY
}
@ -401,6 +441,19 @@ class MusicService : MediaBrowserServiceCompat() {
else stopSelf()
}
private fun seekTo(positionMs: Int) {
mediaPlayer?.let { player ->
try {
if (player.isPlaying || currentStatus) {
player.seekTo(positionMs)
Log.i(TAG, "跳转到位置: ${positionMs}ms")
}
} catch (e: Exception) {
Log.e(TAG, "跳转失败: ${e.message}")
}
}
}
private fun updateNotification(title: String,artist: String,coverUrl: String?) {
val notification = createNotification(title, artist, coverUrl)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

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

@ -70,6 +70,19 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
)
result.success(status)
}
"getProgress" -> {
val progressInfo = MusicService.getProgress()
result.success(progressInfo)
}
"seekTo" -> {
val position = call.argument<Int>("position")
if (position != null) {
MusicServiceStarter.startServiceWithCommand(context, "seekTo", position = position)
result.success(null)
} else {
result.error("INVALID_ARGUMENT", "Missing 'position' parameter", null)
}
}
else -> result.notImplemented()
}
}

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

@ -18,7 +18,7 @@ import java.util.jar.Manifest
object MusicServiceStarter {
private val TAG = "MusicServiceStarter"
//播放单曲
fun startServiceWithCommand(context: Context, command: String, song: Map<String,Any>? = null, volume: Float? = null) {
fun startServiceWithCommand(context: Context, command: String, song: Map<String,Any>? = null, volume: Float? = null, position: Int? = null) {
Log.i(TAG, "liwei------------------- startServiceWithCommand: $command $song")
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
@ -26,6 +26,7 @@ object MusicServiceStarter {
putExtra("song", HashMap(it)) // 只有非 null 时才 putExtra
}
volume?.let { putExtra("volume", it) }
position?.let { putExtra("position", it) }
}
val success = try {
ContextCompat.startForegroundService(context, intent)

30
local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift

@ -75,6 +75,36 @@ public class MusicService: NSObject {
func setVolume(volume: Float) {
player?.volume = volume
}
func getProgress() -> [String: Any] {
guard let player = player, let currentItem = player.currentItem else {
return [
"currentPosition": 0,
"duration": 0,
"progress": 0.0
]
}
let currentTime = CMTimeGetSeconds(player.currentTime())
let duration = CMTimeGetSeconds(currentItem.duration)
// 处理无效值
let currentPosition = currentTime.isNaN || currentTime.isInfinite ? 0 : Int(currentTime * 1000)
let totalDuration = duration.isNaN || duration.isInfinite ? 0 : Int(duration * 1000)
let progress = totalDuration > 0 ? currentTime / duration : 0.0
return [
"currentPosition": currentPosition,
"duration": totalDuration,
"progress": progress.isNaN || progress.isInfinite ? 0.0 : progress
]
}
func seekTo(positionMs: Int) {
guard let player = player else { return }
let time = CMTime(seconds: Double(positionMs) / 1000.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
player.seek(to: time)
}
private func setupRemoteCommandCenter() {
let commandCenter = MPRemoteCommandCenter.shared()

14
local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift

@ -65,6 +65,20 @@ public class MusicServicePlugin: NSObject, FlutterPlugin {
"url": currentUrl != nil ? currentUrl! : NSNull()
]
result(status)
case "getProgress":
let progressInfo = MusicService.shared.getProgress()
result(progressInfo)
case "seekTo":
if let args = call.arguments as? [String: Any],
let position = args["position"] as? Int {
MusicService.shared.seekTo(positionMs: position)
result(nil)
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing or invalid 'position'", details: nil))
}
default:
result(FlutterMethodNotImplemented)
}

15
local_plugins/music_service/lib/music_service.dart

@ -59,4 +59,19 @@ class MusicService {
'url': result?['url'] ?? "",
};
}
/// 获取播放进度信息
Future<Map<String, dynamic>> getProgress() async {
final result = await _methodChannel.invokeMethod<Map>('getProgress');
return {
'currentPosition': result?['currentPosition'] ?? 0, // 当前播放位置(毫秒)
'duration': result?['duration'] ?? 0, // 总时长(毫秒)
'progress': result?['progress'] ?? 0.0, // 播放进度(0.0-1.0)
};
}
/// 跳转到指定位置
Future<void> seekTo(int positionMs) async {
await _methodChannel.invokeMethod('seekTo', {'position': positionMs});
}
}

2
test.json

@ -15,7 +15,7 @@ adb shell am broadcast \
adb shell dumpsys package com.yunqiinnovation.deepsound | grep receiver
adb logcat -v time | grep -i BleForegroundService
adb logcat -v time | grep -Ei "BleWakeReceiver|BleMediaService"

Loading…
Cancel
Save