import 'dart:async'; import 'package:agent_service/agent_service.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '/core/utils/device_connection.dart'; import '/core/utils/id_verify_guard.dart'; import '/core/utils/toast_throttle.dart'; import '../../../core/utils/logger.dart'; import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig_model.dart'; import '../../../data/services/id_verify_service.dart'; import '../../../data/services/novice_guide_service.dart'; import '../../../data/services/ota_check_service.dart'; import '../../../devices/device_hub.dart'; import '../../../routes/app_routes.dart'; /// 主框架控制器:各功能入口的跳转与闸门、连上设备后的固件检查、播报打断开关。 /// /// 设备状态一律来自 [DeviceHub],这里不再自己维护任何连接/电量真相。 class HomeController extends GetxController { static const String _tag = 'HomeCtrl'; static HomeController get to => Get.find(); final GetStorage _storage = GetStorage(); // 播报打断开关状态 final isTtsInterruptEnabled = false.obs; late final OtaCheckService _otaCheckService; final noviceGuideService = Get.find(); Worker? _connectedWorker; /// 任一设备在线。保留旧名给还在用它的页面。 RxBool get isFullyConnectedRx => DeviceHub.to.anyConnected; bool get isFullyConnected => isFullyConnectedRx.value; /// 按设备名安全地查产品(AppConfig.getproduct 单向包含,这里双向)。 DBProduct? _findProductByName(String deviceName) { if (!AppConfig.isInitialized()) return null; final n = deviceName.trim(); if (n.isEmpty) return null; for (final p in AppConfig.config.products) { if (p.devicename.contains(n) || n.contains(p.devicename)) return p; } return null; } @override void onInit() { super.onInit(); _otaCheckService = Get.find(); // 实名状态:InitialBinding 里那次拉取发生在登录之前、必然拿不到, // 登录后所有流程都会落到 home,在这里补一次(已拉到过就不会重复请求)。 if (Get.isRegistered()) { IdVerifyService.to.ensureLoaded(); } _loadTtsInterrupt(); _storage.listenKey('agent_tts_interrupt', (value) { try { final bool newValue = value is bool ? value : (value == 'true'); if (isTtsInterruptEnabled.value != newValue) { isTtsInterruptEnabled.value = newValue; } } catch (_) {} }); // 连接成功后检查 OTA 升级 _connectedWorker = ever(DeviceHub.to.anyConnected, (on) { if (on && noviceGuideService.isHomeGuide) { Future.delayed(const Duration(seconds: 1), _checkOtaUpgrade); } }); } @override void onClose() { _connectedWorker?.dispose(); super.onClose(); } /// 加载"播报打断"开关状态:本地没存过时按设备产品的 iscontinuouschat 取反。 Future _loadTtsInterrupt() async { try { if (!_storage.hasData('agent_tts_interrupt')) { final deviceName = getDeviceName(); final DBProduct? product = _findProductByName(deviceName); if (deviceName.isNotEmpty && product != null) { isTtsInterruptEnabled.value = !(product.iscontinuouschat ?? true); } else { isTtsInterruptEnabled.value = false; } } else { isTtsInterruptEnabled.value = _storage.read('agent_tts_interrupt') ?? false; } await AgentService.isInterrupt(isTtsInterruptEnabled.value); } catch (e) { Logger.e(_tag, '加载播报打断开关失败: $e'); isTtsInterruptEnabled.value = false; } } Future toggleTtsInterrupt() async { try { isTtsInterruptEnabled.toggle(); await _storage.write('agent_tts_interrupt', isTtsInterruptEnabled.value); await AgentService.isInterrupt(isTtsInterruptEnabled.value); } catch (e) { Logger.e(_tag, '保存播报打断偏好失败: $e'); } } /// 刷新设备状态(点刷新按钮 / 定时) Future refreshDeviceConnection() => DeviceHub.to.refreshAll(); void rescanDevices() { Get.toNamed(Routes.deviceCategory); } void openTranslation() { AgentService.interruptCurrentResponse(); Get.toNamed(Routes.translationMiddleware); } /// 翻译跳转:按 translationId 进入对应翻译模式 /// translationId 取值:simultaneous / faceToFace / call / audioVideo /// 其中 call 依赖设备连接且要正处于通话中 void openTranslationFeature(String translationId) { if (IdVerifyGuard.blocked()) return; final bool needDevice = translationId == 'call'; // 下面三条一律走 ToastThrottle:卡片只是灰着、点得动,用户会连点几下试 if (needDevice && DeviceConnection.any && !DeviceConnection.inCall) { ToastThrottle.show(ToastThrottle.keyCallRequired, title: 'callRequiredTitle'.tr, message: 'callRequiredDesc'.tr, window: ToastThrottle.gateWindow); return; } if (needDevice && !DeviceConnection.any) { ToastThrottle.show(ToastThrottle.keyConnectDevice, title: 'connectDevice'.tr, message: 'pleaseConnectDeepSoundDevice'.tr, window: ToastThrottle.gateWindow); return; } if (DeviceConnection.isBlockedByCall(translationId)) { // ⚠️ 这条闸门卡住过用户好几次(「电话早挂了,同传还是点不进去」), // 而 inCall 的来路很长:耳机帧 → isCallOngoing → feature 事件 → DeviceHub.inCall。 // 被拦时把当时的判据原样打出来,省得再靠推演。 Logger.w('HomeController', '[闸门] $translationId 被拦: inCall=${DeviceConnection.inCall} ' 'anyDevice=${DeviceConnection.any} device=${DeviceConnection.connectedName}'); ToastThrottle.show(ToastThrottle.keyInCallBusy, title: 'inCallBusyTitle'.tr, message: 'inCallBusyDesc'.tr, window: ToastThrottle.gateWindow); return; } AgentService.interruptCurrentResponse(); Get.toNamed(Routes.translation, arguments: {'translationId': translationId}); } /// 打开通话录音:与卡片置灰条件统一,未连接设备/不在通话中则提示并返回 void openCallRecording() { if (IdVerifyGuard.blocked()) return; if (DeviceConnection.any && !DeviceConnection.inCall) { ToastThrottle.show(ToastThrottle.keyCallRequired, title: 'callRequiredTitle'.tr, message: 'callRequiredDesc'.tr, window: ToastThrottle.gateWindow); return; } if (!DeviceConnection.any) { ToastThrottle.show(ToastThrottle.keyConnectDevice, title: 'connectDevice'.tr, message: 'pleaseConnectDeepSoundDevice'.tr, window: ToastThrottle.gateWindow); return; } Get.toNamed(Routes.recording, arguments: {'title': 'agentCallRecording', 'folder': 'call_recordings'}); } /// 打开现场录音。不要求先连设备:连上了走设备录音通路,没连就用手机麦克风。 void openFieldRecording() { if (DeviceConnection.isBlockedByCall('fieldRecording')) { ToastThrottle.show(ToastThrottle.keyInCallBusy, title: 'inCallBusyTitle'.tr, message: 'inCallBusyDesc'.tr, window: ToastThrottle.gateWindow); return; } if (IdVerifyGuard.blocked()) return; Get.toNamed(Routes.fieldRecording, arguments: { 'title': 'agentFieldRecording', 'folder': 'field_recordings', 'archiveToMeeting': true, }); } /// 打开会议模块 void openMeet() { if (IdVerifyGuard.blocked()) return; AgentService.interruptCurrentResponse(); Get.toNamed(Routes.meeting); } Color getConnectionStatusColor() => isFullyConnected ? Colors.green : Colors.grey; String getConnectionStatusText() => isFullyConnected ? 'connected'.tr : 'unconnected'.tr; /// 当前(或最近一次)连接设备的名字;从没连过返回「未知设备」 String getDeviceName() { final n = DeviceHub.to.lastKnownName; return n.isNotEmpty ? n : 'unknownEquipment'.tr; } String getDeviceId() => DeviceHub.to.primaryInfo.value?.macAddress ?? DeviceHub.to.primaryInfo.value?.id ?? 'unknown'.tr; String getVersionText() { final v = DeviceHub.to.primaryInfo.value?.firmwareVersion ?? ''; return v.isNotEmpty ? v : 'unknown'.tr; } Future _checkOtaUpgrade() async { final success = await _otaCheckService.performOtaCheck(isFullyConnected); Logger.d(_tag, success ? 'OTA检查已成功执行' : 'OTA检查被跳过或执行失败'); } Map getOtaCheckStatus() => _otaCheckService.getCheckStatus(); }