You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
242 lines
8.6 KiB
242 lines
8.6 KiB
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<HolderScanView> createState() => _HolderScanViewState();
|
|
}
|
|
|
|
class _HolderScanViewState extends State<HolderScanView> {
|
|
DevicePlugin get _plugin => DeviceHub.to.plugin(DeviceVendors.holder)!;
|
|
final RxList<DiscoveredDevice> _scanned = <DiscoveredDevice>[].obs;
|
|
final RxBool _isScanning = false.obs;
|
|
StreamSubscription<DevicePluginEvent>? _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<void> _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<void> _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>()) {
|
|
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)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|