182 lines
6.2 KiB
Dart
182 lines
6.2 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
|||
|
|
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
|
|||
|
|
import 'package:starcloud/entity/star_cloud_lock_list.dart' as scl;
|
|||
|
|
import '../l10n.dart';
|
|||
|
|
import '../global.dart';
|
|||
|
|
import '../widgets/common_widgets.dart';
|
|||
|
|
|
|||
|
|
class DeviceScanTab extends StatefulWidget {
|
|||
|
|
const DeviceScanTab({super.key});
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
State<DeviceScanTab> createState() => _DeviceScanTabState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class _DeviceScanTabState extends State<DeviceScanTab> {
|
|||
|
|
bool _scanning = false;
|
|||
|
|
bool _pairing = false;
|
|||
|
|
List<dynamic> _devices = [];
|
|||
|
|
final List<String> _log = [];
|
|||
|
|
|
|||
|
|
void _log_(String msg) {
|
|||
|
|
final ts = DateTime.now().toIso8601String().substring(11, 19);
|
|||
|
|
setState(() => _log.add('[$ts] $msg'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<void> _scan() async {
|
|||
|
|
if (!sdkCloudAccountReady.value) {
|
|||
|
|
_log_(S.isEn
|
|||
|
|
? 'Set an SDK cloud account before scanning and pairing a lock.'
|
|||
|
|
: '请先在“登录”页创建或设置 SDK 星云账户,再扫描绑定锁。');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setState(() {
|
|||
|
|
_scanning = true;
|
|||
|
|
_devices = [];
|
|||
|
|
});
|
|||
|
|
_log_(S.isEn ? 'Scan start (10s)...' : '开始扫描(10 秒超时)...');
|
|||
|
|
try {
|
|||
|
|
await StarCloudClient.instance.device.scanDeviceList(
|
|||
|
|
timeout: 10,
|
|||
|
|
onScan: (results) {
|
|||
|
|
setState(() => _devices = results);
|
|||
|
|
});
|
|||
|
|
_log_(S.isEn
|
|||
|
|
? 'Done. Found ${_devices.length}'
|
|||
|
|
: '扫描完成,发现 ${_devices.length} 个设备');
|
|||
|
|
} catch (e) {
|
|||
|
|
_log_('${S.isEn ? "Failed" : "失败"}: $e');
|
|||
|
|
} finally {
|
|||
|
|
setState(() => _scanning = false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<void> _pair(int idx) async {
|
|||
|
|
if (!sdkCloudAccountReady.value) {
|
|||
|
|
_log_(
|
|||
|
|
S.isEn ? 'Please set an SDK cloud account first.' : '请先设置 SDK 星云账户。');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setState(() => _pairing = true);
|
|||
|
|
_log_(S.isEn ? 'Pairing...' : '正在配对...');
|
|||
|
|
try {
|
|||
|
|
final lock = await StarCloudClient.instance.device
|
|||
|
|
.pairDevice(scanResult: _devices[idx]);
|
|||
|
|
// 更新 ValueNotifier 的值
|
|||
|
|
pairedLockId.value = lock.lockId;
|
|||
|
|
pairedLock.value = lock;
|
|||
|
|
|
|||
|
|
_log_('${S.isEn ? "Success" : "成功"}! ID: ${lock.lockId}');
|
|||
|
|
} catch (e) {
|
|||
|
|
_log_('${S.isEn ? "Failed" : "失败"}: $e');
|
|||
|
|
} finally {
|
|||
|
|
setState(() => _pairing = false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
return ValueListenableBuilder<bool>(
|
|||
|
|
valueListenable: S.langNotifier,
|
|||
|
|
builder: (context, isEn, _) {
|
|||
|
|
return Column(
|
|||
|
|
children: [
|
|||
|
|
Padding(
|
|||
|
|
padding: const EdgeInsets.all(12),
|
|||
|
|
child: Row(
|
|||
|
|
children: [
|
|||
|
|
Expanded(
|
|||
|
|
child: ElevatedButton.icon(
|
|||
|
|
onPressed: _scanning ? null : _scan,
|
|||
|
|
icon: _scanning
|
|||
|
|
? const SizedBox(
|
|||
|
|
width: 16,
|
|||
|
|
height: 16,
|
|||
|
|
child: CircularProgressIndicator(strokeWidth: 2))
|
|||
|
|
: const Icon(Icons.bluetooth_searching),
|
|||
|
|
label: Text(_scanning ? S.btnScanning : S.btnScan),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
const SizedBox(width: 8),
|
|||
|
|
ElevatedButton(
|
|||
|
|
onPressed: _scanning
|
|||
|
|
? () => StarCloudClient.instance.device
|
|||
|
|
.stopScan()
|
|||
|
|
.then((_) => setState(() => _scanning = false))
|
|||
|
|
: null,
|
|||
|
|
child: Text(S.btnStop)),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
// 使用 ValueListenableBuilder 监听配对状态
|
|||
|
|
ValueListenableBuilder<int?>(
|
|||
|
|
valueListenable: pairedLockId,
|
|||
|
|
builder: (context, id, _) {
|
|||
|
|
if (id == null) return const SizedBox.shrink();
|
|||
|
|
return Container(
|
|||
|
|
padding:
|
|||
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|||
|
|
alignment: Alignment.centerLeft,
|
|||
|
|
child: Chip(
|
|||
|
|
label: Text('${S.pairedStatus}: #$id'),
|
|||
|
|
backgroundColor: Colors.green.shade50,
|
|||
|
|
onDeleted: () {
|
|||
|
|
pairedLockId.value = null;
|
|||
|
|
pairedLock.value = null;
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
Expanded(
|
|||
|
|
child: ValueListenableBuilder<scl.StarCloudLock?>(
|
|||
|
|
valueListenable: pairedLock,
|
|||
|
|
builder: (context, paired, _) {
|
|||
|
|
if (_devices.isEmpty) {
|
|||
|
|
return Center(
|
|||
|
|
child: Text(
|
|||
|
|
S.scanHint,
|
|||
|
|
style: const TextStyle(color: Colors.grey),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
return ListView.separated(
|
|||
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|||
|
|
itemCount: _devices.length,
|
|||
|
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
|||
|
|
itemBuilder: (context, i) {
|
|||
|
|
final device = _devices[i] as StarCloudScanResult;
|
|||
|
|
final isPaired = _isPairedDevice(device, paired);
|
|||
|
|
return ListTile(
|
|||
|
|
title: Text(device.advName.isNotEmpty
|
|||
|
|
? device.advName
|
|||
|
|
: (S.isEn ? 'Unknown' : '未知设备')),
|
|||
|
|
subtitle: Text('MAC: ${device.id}'),
|
|||
|
|
trailing: ElevatedButton(
|
|||
|
|
onPressed:
|
|||
|
|
_pairing || isPaired ? null : () => _pair(i),
|
|||
|
|
child: Text(isPaired ? S.pairedStatus : S.btnPair),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
LogArea(log: _log),
|
|||
|
|
],
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool _isPairedDevice(
|
|||
|
|
StarCloudScanResult device,
|
|||
|
|
scl.StarCloudLock? paired,
|
|||
|
|
) {
|
|||
|
|
if (paired == null) return false;
|
|||
|
|
return paired.mac == device.id ||
|
|||
|
|
paired.bluetooth.bluetoothDeviceName == device.advName;
|
|||
|
|
}
|
|||
|
|
}
|