import 'dart:async'; import 'package:device_plugin_interface/device_plugin_interface.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../../../core/theme/app_palette.dart'; import '../../../data/services/background_launch_consent.dart'; import '../../../devices/device_hub.dart'; import '../../../devices/device_vendors.dart'; import '../../../routes/app_routes.dart'; import '../../main_tab/controllers/main_tab_controller.dart'; /// EaiRec 录音支架(AI手机支架)的扫描 / 连接页——走 [DeviceHub] 里的支架插件。 class HolderScanView extends StatefulWidget { const HolderScanView({super.key}); @override State createState() => _HolderScanViewState(); } class _HolderScanViewState extends State { DevicePlugin get _plugin => DeviceHub.to.plugin(DeviceVendors.holder)!; final RxList _scanned = [].obs; final RxBool _isScanning = false.obs; StreamSubscription? _sub; bool _connecting = false; @override void initState() { super.initState(); _sub = _plugin.eventStream.listen((e) { switch (e.type) { case DevicePluginEventType.scanStarted: _isScanning.value = true; break; case DevicePluginEventType.scanStopped: _isScanning.value = false; break; case DevicePluginEventType.deviceDiscovered: final d = e.discovered; if (d == null || d.id.isEmpty) break; // 按地址去重累积:原生是「每扫到一个就单独上报一次」 final i = _scanned.indexWhere((x) => x.id == d.id); if (i >= 0) { _scanned[i] = d; } else { _scanned.add(d); } break; default: break; } }); // 进页面自动开扫 WidgetsBinding.instance.addPostFrameCallback((_) => _startScan()); } Future _startScan() async { _scanned.clear(); if (!await _plugin.ensureReady()) { Get.snackbar('tip'.tr, 'holderPairHint'.tr, snackPosition: SnackPosition.TOP); return; } await _plugin.startScan(); } @override void dispose() { _sub?.cancel(); _plugin.stopScan(); super.dispose(); } Future _connect(DiscoveredDevice d) async { if (_connecting) return; // Android 上 SDK 的 connect() 本身就是向系统注册后台扫连(设备靠近就唤醒 // 本应用),属于自启动行为,必须先向用户明示并取得同意。 if (!await BackgroundLaunchConsent.ensure(userInitiated: true)) { Get.snackbar( 'autoConnectRequired'.tr, BackgroundLaunchConsent.declined ? 'autoConnectReenableHint'.tr : 'autoConnectRequiredDesc'.tr, snackPosition: SnackPosition.TOP); return; } setState(() => _connecting = true); bool ok = false; try { await _plugin.connect(d.id); ok = true; } on DeviceException catch (e) { debugPrint('holder connect failed: ${e.code} ${e.message}'); } catch (e) { debugPrint('holder connect failed: $e'); } if (!mounted) return; setState(() => _connecting = false); if (ok) { Get.snackbar('连接成功', '${d.name} 已连接', snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 2)); Get.offAllNamed(Routes.home); WidgetsBinding.instance.addPostFrameCallback((_) { if (Get.isRegistered()) { MainTabController.to.showDeviceAfterConnect(); } }); } else { Get.snackbar('连接失败', 'holderRetryHint'.tr, snackPosition: SnackPosition.TOP); } } @override Widget build(BuildContext context) { final bool isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: AppPalette.background(isDark), appBar: AppBar( backgroundColor: AppPalette.background(isDark), elevation: 0, title: Text('holderAddTitle'.tr), actions: [ Obx(() => _isScanning.value ? Padding( padding: EdgeInsets.only(right: 16.w), child: Center( child: SizedBox( width: 18.w, height: 18.w, child: const CircularProgressIndicator(strokeWidth: 2), ), ), ) : IconButton( icon: const Icon(Icons.refresh), onPressed: _startScan, )), ], ), body: _connecting ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text('holderConnecting'.tr), ], ), ) : Obx(() { final list = _scanned; if (list.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.bluetooth_searching, size: 56.sp, color: AppPalette.brand300), SizedBox(height: 12.h), Text( _isScanning.value ? 'holderSearching'.tr : 'holderNotFound'.tr, style: TextStyle( fontSize: 15.sp, color: AppPalette.textSecondary(isDark)), ), SizedBox(height: 6.h), Text( 'holderPairHint'.tr, style: TextStyle( fontSize: 12.sp, color: AppPalette.textHint(isDark)), ), ], ), ); } return ListView.separated( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), itemCount: list.length, separatorBuilder: (_, __) => SizedBox(height: 12.h), itemBuilder: (context, i) { final d = list[i]; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _connect(d), child: Container( padding: EdgeInsets.all(14.w), decoration: BoxDecoration( color: AppPalette.surface(isDark), borderRadius: BorderRadius.circular(14.r), border: Border.all(color: AppPalette.border(isDark)), ), child: Row( children: [ Icon(Icons.stay_current_portrait, size: 26.sp, color: AppPalette.accent), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( d.name.isEmpty ? 'bleUnknownDevice'.tr : d.name, style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.w600, color: AppPalette.textPrimary(isDark), ), ), SizedBox(height: 2.h), Text( d.id, style: TextStyle( fontSize: 11.sp, color: AppPalette.textHint(isDark), ), ), ], ), ), Text('连接', style: TextStyle( fontSize: 13.sp, fontWeight: FontWeight.w600, color: AppPalette.accent)), ], ), ), ); }, ); }), ); } }