diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6f823e086..ed13eec0f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -251,7 +251,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -346,14 +346,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -367,14 +363,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -789,7 +781,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 2285cd932..042bb3f72 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -1733,6 +1733,8 @@ const Map enUS = { "deviceNotAuth": "Device not authenticated", "microphoneName": "Microphone", "mobileElfMicPermission": "Mobile Elf needs microphone permission for voice chat", + "deviceNotConnected": "Device not connected", + "mobileElfNeedDevice": "Please connect your Mobile device to use AI features", "micPermissionSettings": "Please enable microphone permission in Settings to use voice features", "weekdayMon": "Monday", "weekdayTue": "Tuesday", diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 47b3ff101..a443dea5b 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -1572,6 +1572,8 @@ const Map zhCN = { "deviceNotAuth": "设备未认证", "microphoneName": "麦克风", "mobileElfMicPermission": "移动精灵需要麦克风权限才能进行语音对话", + "deviceNotConnected": "设备未连接", + "mobileElfNeedDevice": "请连接移动设备后使用 AI 功能", "micPermissionSettings": "请在设置中开启麦克风权限以使用语音功能", "weekdayMon": "星期一", "weekdayTue": "星期二", diff --git a/lib/data/models/appconfig.dart b/lib/data/models/appconfig.dart index 8ce31d29f..8376d9cd3 100644 --- a/lib/data/models/appconfig.dart +++ b/lib/data/models/appconfig.dart @@ -1,13 +1,15 @@ import 'dart:convert'; +import 'package:get/get.dart'; + import '../../../data/models/appconfig_model.dart'; class AppConfig { - // 中文注释:是否隐藏"AI全能助手"模块。 + // 是否隐藏"AI全能助手"模块(响应式)。 // true => 首页/发现页切换为精简布局,新手引导跳过 AI 引导步骤 // false => 保持原有布局 - // TODO: 后续接入 BLE PID 判断逻辑,B017 设备连接时自动置 true - static bool hideAiAssistant = false; + // 数据来源:连接的 BLE 设备对应 DBProduct.hideaiassistant,由 BleManager 同步。 + static final RxBool hideAiAssistant = false.obs; static late UserGetAppConfigResp config; diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 747ab4cc4..cb491e0eb 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -212,6 +212,7 @@ class BleManager extends GetxService { final _isCall = false.obs; final _isConnected = false.obs; bool get isConnected => _isConnected.value; + RxBool get isConnectedRx => _isConnected; bool get isCall => _isCall.value; // 设备信息缓存 final deviceInfo = Rx(null); // 使用 Rx 包装 @@ -260,6 +261,31 @@ class BleManager extends GetxService { // 设置BLE事件监听 _setupBleListener(); + + // 同步 AI 助手隐藏开关:deviceInfo 拿到名字后按 DBProduct 配置写入; + // 断连时 deviceInfo 会被置 null,统一在断连分支复位。 + ever(deviceInfo, _syncHideAiAssistant); + } + + /// 按当前连接设备同步 AppConfig.hideAiAssistant + void _syncHideAiAssistant(DeviceInfo? info) { + if (info == null || info.name.isEmpty) { + if (AppConfig.hideAiAssistant.value) { + AppConfig.hideAiAssistant.value = false; + } + return; + } + try { + final product = AppConfig.getproduct(info.name); + final hide = product?.hideaiassistant ?? false; + if (AppConfig.hideAiAssistant.value != hide) { + AppConfig.hideAiAssistant.value = hide; + Logger.i('BleManager', + 'hideAiAssistant 同步: device=${info.name} hide=$hide'); + } + } catch (e) { + Logger.e('BleManager', 'hideAiAssistant 同步失败: $e'); + } } /// 释放资源 diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index 2748855fa..a0a9c1287 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -66,9 +66,9 @@ class ExploreView extends GetView { // AI功能列表 SizedBox(height: 24.h), - AppConfig.hideAiAssistant + Obx(() => AppConfig.hideAiAssistant.value ? _buildSimplifiedAILayout(isDarkMode) - : _buildCustomAILayout(isDarkMode), + : _buildCustomAILayout(isDarkMode)), ], ), ), diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 77e162948..a60eedc06 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -145,9 +145,9 @@ class HomeView extends GetView { // 热门AI体验 _buildSectionTitle(isDarkMode), SizedBox(height: 16.h), - AppConfig.hideAiAssistant + Obx(() => AppConfig.hideAiAssistant.value ? _buildSimplifiedAILayout() - : _buildPremiumAILayout(), + : _buildPremiumAILayout()), ], ), ), diff --git a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart index 9ca497e6e..5008be90b 100644 --- a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart +++ b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart @@ -45,6 +45,10 @@ class MobileElfController extends GetxController { final credentialFailed = false.obs; final credentialFailedReason = ''.obs; + // 设备连接状态(直接转发 BleManager,UI 用 Obx 监听) + RxBool get deviceConnectedRx => BleManager.to.isConnectedRx; + bool get isDeviceConnected => BleManager.to.isConnected; + // ── 内部 ─────────────────────────────────────────────────────────────────── StreamSubscription? _eventSub; Timer? _callTimer; @@ -256,6 +260,10 @@ class MobileElfController extends GetxController { Future startCall() async { if (inCall.value || _callStarting) return; + if (!isDeviceConnected) { + _promptDeviceRequired(); + return; + } _callStarting = true; _pttAborted = false; _checkHeartbeat(); @@ -314,6 +322,19 @@ class MobileElfController extends GetxController { await AgentService.stopConversation(); } + /// 未连接设备时统一的拦截提示(startCall / startPtt 共用) + void _promptDeviceRequired() { + Get.snackbar( + 'deviceNotConnected'.tr, + 'mobileElfNeedDevice'.tr, + snackPosition: SnackPosition.BOTTOM, + backgroundColor: const Color(0xCC1F2937), + colorText: const Color(0xFFFFFFFF), + margin: const EdgeInsets.all(16), + duration: const Duration(seconds: 2), + ); + } + /// 超过1小时未上报心跳则补报一次 void _checkHeartbeat() { final last = _storage.read('sts_heartbeat_time') ?? 0; @@ -324,6 +345,10 @@ class MobileElfController extends GetxController { Future startPtt() async { if (pttRecording.value || _pttStarting) return; + if (!isDeviceConnected) { + _promptDeviceRequired(); + return; + } _pttStarting = true; _pttAborted = false; _checkHeartbeat(); diff --git a/lib/modules/mobile_elf/views/mobile_elf_view.dart b/lib/modules/mobile_elf/views/mobile_elf_view.dart index a42b8da67..08b2862e1 100644 --- a/lib/modules/mobile_elf/views/mobile_elf_view.dart +++ b/lib/modules/mobile_elf/views/mobile_elf_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; // 用于 DateFormat +import '../../../data/services/ble_manager.dart'; import '../controllers/mobile_elf_controller.dart'; class MobileElfView extends GetView { @@ -144,6 +145,18 @@ class MobileElfView extends GetView { /// 右上角状态胶囊 + 通话计时 Widget _buildStatusPill() { + // 未连接设备优先展示(用户更直观的可恢复状态) + if (!BleManager.to.isConnectedRx.value) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 3.h), + decoration: BoxDecoration(color: const Color(0xFFFEF2F2), borderRadius: BorderRadius.circular(20.r)), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.bluetooth_disabled_rounded, size: 11.sp, color: _red), + SizedBox(width: 4.w), + Text('deviceNotConnected'.tr, style: TextStyle(fontSize: 11.sp, color: _red, fontWeight: FontWeight.w500)), + ]), + ); + } if (controller.credentialFailed.value) { return Container( padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 3.h), @@ -483,6 +496,7 @@ class MobileElfView extends GetView { child: Column( mainAxisSize: MainAxisSize.min, children: [ + if (!BleManager.to.isConnectedRx.value) _buildDeviceRequiredBar(isDark), if (showStatusBar) _buildStatusBar(s, inCall, isDark), if (inCall) _buildInCallInputBar(isDark) @@ -495,6 +509,28 @@ class MobileElfView extends GetView { }); } + // ── 未连接设备警示横幅 ──────────────────────────────────────────────────── + + Widget _buildDeviceRequiredBar(bool isDark) { + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + color: const Color(0xFFFEF2F2), + child: Row( + children: [ + Icon(Icons.bluetooth_disabled_rounded, size: 16.sp, color: _red), + SizedBox(width: 8.w), + Expanded( + child: Text( + 'mobileElfNeedDevice'.tr, + style: TextStyle(fontSize: 13.sp, color: _red, fontWeight: FontWeight.w500), + ), + ), + ], + ), + ); + } + // ── 状态提示栏 ──────────────────────────────────────────────────────────── Widget _buildStatusBar(MobileElfStatus s, bool inCall, bool isDark) { @@ -633,7 +669,8 @@ class MobileElfView extends GetView { final s = controller.status.value; final ptt = controller.pttRecording.value; - final locked = controller.credentialFailed.value; + final notConnected = !BleManager.to.isConnectedRx.value; + final locked = controller.credentialFailed.value || notConnected; final disabled = locked || (!ptt && s != MobileElfStatus.idle && s != MobileElfStatus.error); final pttClickable = !locked && s == MobileElfStatus.speaking; final sideDisabled = locked || ptt || (disabled && !pttClickable);