cashumit/lib/widgets/bluetooth_scan_dialog.dart

210 lines
6.6 KiB
Dart
Raw Permalink Normal View History

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:bluetooth_print/bluetooth_print_model.dart';
import '../services/bluetooth_service.dart';
class BluetoothScanDialog extends StatefulWidget {
final BluetoothService bluetoothService;
final void Function(BluetoothDevice device, bool success)? onDeviceConnected;
const BluetoothScanDialog({
super.key,
required this.bluetoothService,
this.onDeviceConnected,
});
@override
State<BluetoothScanDialog> createState() => _BluetoothScanDialogState();
}
class _BluetoothScanDialogState extends State<BluetoothScanDialog> {
bool _isScanning = false;
List<BluetoothDevice> _scannedDevices = [];
String? _connectingAddress;
StreamSubscription<List<BluetoothDevice>>? _scanResultsSub;
StreamSubscription<bool>? _isScanningSub;
@override
void initState() {
super.initState();
// 1. Langsung berlangganan ke stream hasil scan agar perangkat yang ditemukan seketika tampil di UI
_scanResultsSub = widget.bluetoothService.scanResults.listen((devices) {
if (mounted) {
setState(() {
_scannedDevices = devices;
});
}
});
// 2. Berlangganan ke stream status scan untuk sinkronisasi indikator loading
_isScanningSub = widget.bluetoothService.isScanning.listen((scanning) {
if (mounted) {
setState(() {
_isScanning = scanning;
});
}
});
// 3. Mulai pemindaian perdana tepat 1 kali saat dialog dibuka
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_startScan();
}
});
}
@override
void dispose() {
_scanResultsSub?.cancel();
_isScanningSub?.cancel();
widget.bluetoothService.stopScan();
super.dispose();
}
Future<void> _startScan() async {
if (_connectingAddress != null) return;
if (mounted) {
setState(() {
_isScanning = true;
_scannedDevices.clear();
});
}
try {
await widget.bluetoothService.startScan(timeout: const Duration(seconds: 5));
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal memindai: $e')),
);
}
}
}
Future<void> _connect(BluetoothDevice device) async {
if (_connectingAddress != null) return;
setState(() {
_connectingAddress = device.address;
});
// Hentikan pemindaian terlebih dahulu sebelum menyambungkan ke BLE device
await widget.bluetoothService.stopScan();
final ok = await widget.bluetoothService.connectToDevice(device);
if (mounted) {
widget.onDeviceConnected?.call(device, ok);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: true,
onPopInvokedWithResult: (didPop, _) {
if (didPop) {
widget.bluetoothService.stopScan();
}
},
child: AlertDialog(
title: Row(
children: [
const Icon(Icons.bluetooth_searching, color: Colors.blue),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Pindai Printer Bluetooth',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
if (_isScanning)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
content: SizedBox(
width: MediaQuery.of(context).size.width,
height: 320,
child: Column(
children: [
if (_isScanning)
const Padding(
padding: EdgeInsets.symmetric(vertical: 6.0),
child: Text(
'Mencari printer thermal di sekitar...',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
),
Expanded(
child: _scannedDevices.isEmpty
? Center(
child: _isScanning
? const Text('Sedang memindai perangkat...')
: const Text(
'Tidak ada perangkat Bluetooth ditemukan.\nPastikan Bluetooth & printer aktif.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
)
: ListView.builder(
itemCount: _scannedDevices.length,
itemBuilder: (context, index) {
final dev = _scannedDevices[index];
final isConnectingThis = _connectingAddress == dev.address;
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: const Icon(Icons.print, color: Colors.blue),
title: Text(
dev.name ?? 'Perangkat Bluetooth',
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(dev.address ?? ''),
trailing: ElevatedButton(
onPressed: (_connectingAddress != null)
? null
: () => _connect(dev),
child: isConnectingThis
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Hubungkan'),
),
),
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: (_isScanning || _connectingAddress != null)
? null
: () => _startScan(),
child: const Text('Pindai Ulang'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Tutup'),
),
],
),
);
}
}