import 'package:echomeet_device_sdk/echomeet_device_sdk.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 '../../../data/services/holder_device_service.dart'; import '../../../routes/app_routes.dart'; import '../../main_tab/controllers/main_tab_controller.dart'; /// 蓝讯方案设备(AI手机支架)的扫描 / 连接页。 /// /// 走的是从 echomeet 拉过来的 [EchoMeetDeviceSdk],与杰理耳机的扫描页 /// (DevicesView)是两条独立链路,互不干扰。 class HolderScanView extends StatefulWidget { const HolderScanView({super.key}); @override State createState() => _HolderScanViewState(); } class _HolderScanViewState extends State { late final HolderDeviceService _service; @override void initState() { super.initState(); _service = Get.isRegistered() ? Get.find() : Get.put(HolderDeviceService()); // 进页面自动开扫 WidgetsBinding.instance.addPostFrameCallback((_) => _service.startScan()); } @override void dispose() { _service.stopScan(); super.dispose(); } bool _connecting = false; Future _connect(EchoMeetScannedDevice 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); // 用户主动点了连接:允许再申请一次系统蓝牙权限 _service.resetPermissionDenied(); // connect() 只是注册后台扫连,真正连上要等 connectionState final registered = await _service.connect(d); final ok = registered && await _service.waitConnected(); 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.changeTab(MainTabController.tabDevice); } }); } 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(() => _service.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: _service.startScan, )), ], ), body: _connecting ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('holderConnecting'.tr), ], ), ) : Obx(() { final list = _service.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( _service.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.displayName, style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.w600, color: AppPalette.textPrimary(isDark), ), ), SizedBox(height: 2.h), Text( d.address, style: TextStyle( fontSize: 11.sp, color: AppPalette.textHint(isDark), ), ), ], ), ), Text('连接', style: TextStyle( fontSize: 13.sp, fontWeight: FontWeight.w600, color: AppPalette.accent)), ], ), ), ); }, ); }), ); } }