import 'package:abawo_bt_app/controller/bluetooth.dart'; import 'package:abawo_bt_app/model/shifter_types.dart'; import 'package:flutter/material.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class BikeScanDialog extends ConsumerStatefulWidget { const BikeScanDialog({ required this.excludedDeviceId, super.key, }); final String excludedDeviceId; static Future show( BuildContext context, { required String excludedDeviceId, }) { return showDialog( context: context, barrierDismissible: true, builder: (_) => BikeScanDialog(excludedDeviceId: excludedDeviceId), ); } @override ConsumerState createState() => _BikeScanDialogState(); } class _BikeScanDialogState extends ConsumerState { bool _showAll = false; BluetoothController? _controller; @override void initState() { super.initState(); _startScan(); } Future _startScan() async { final controller = await ref.read(bluetoothProvider.future); _controller = controller; await controller.stopScan(); await controller.startScan(); } @override void dispose() { _controller?.stopScan(); super.dispose(); } @override Widget build(BuildContext context) { final btAsync = ref.watch(bluetoothProvider); final size = MediaQuery.of(context).size; final dialogWidth = size.width < 640 ? size.width - 24 : 560.0; final dialogHeight = size.height < 780 ? size.height * 0.78 : 560.0; return Dialog( clipBehavior: Clip.antiAlias, insetPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), child: SizedBox( width: dialogWidth, height: dialogHeight, child: btAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => Center(child: Text('Bluetooth error: $err')), data: (controller) { _controller ??= controller; return Column( children: [ _DialogHeader( showAll: _showAll, onChanged: (value) { setState(() { _showAll = value; }); }, onRescan: _startScan, ), Expanded( child: StreamBuilder>( stream: controller.scanResultsStream, initialData: controller.scanResults, builder: (context, snapshot) { final devices = _filteredDevices(snapshot.data ?? const []); if (devices.isEmpty) { return const Padding( padding: EdgeInsets.all(20), child: Center( child: Text( 'No matching trainers nearby. Keep scanning or enable Show All to inspect every device in range.', textAlign: TextAlign.center, ), ), ); } return ListView.separated( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), itemCount: devices.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) { final device = devices[index]; final isFtms = device.serviceUuids.contains(Uuid.parse(ftmsServiceUuid)); return Material( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(22), child: InkWell( borderRadius: BorderRadius.circular(22), onTap: () => Navigator.of(context).pop(device), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(22), border: Border.all( color: Theme.of(context) .colorScheme .outlineVariant .withValues(alpha: 0.55), ), ), child: Row( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( shape: BoxShape.circle, color: Theme.of(context) .colorScheme .primary .withValues(alpha: 0.12), ), child: Icon( Icons.pedal_bike_rounded, color: Theme.of(context) .colorScheme .primary, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( device.name.isEmpty ? 'Unknown Device' : device.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) .textTheme .titleMedium ?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), Text( isFtms ? 'FTMS' : 'Nearby trainer', style: Theme.of(context) .textTheme .bodyMedium ?.copyWith( color: Theme.of(context) .colorScheme .primary, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( device.id, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) .textTheme .bodySmall ?.copyWith( color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.62), ), ), ], ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ _RssiBadge(rssi: device.rssi), const SizedBox(height: 12), Icon( Icons.chevron_right_rounded, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.55), ), ], ), ], ), ), ), ); }, ); }, ), ), ], ); }, ), ), ); } List _filteredDevices(List devices) { final ftmsUuid = Uuid.parse(ftmsServiceUuid); return devices.where((device) { if (device.id == widget.excludedDeviceId) { return false; } if (_showAll) { return true; } return device.serviceUuids.contains(ftmsUuid); }).toList(growable: false); } } class _DialogHeader extends StatelessWidget { const _DialogHeader({ required this.showAll, required this.onChanged, required this.onRescan, }); final bool showAll; final ValueChanged onChanged; final VoidCallback onRescan; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Assign Trainer', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), Text( 'Tap a nearby trainer to assign it to the connected shifter.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.68), ), ), ], ), ), IconButton( tooltip: 'Close', onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: Row( children: [ Text( 'Show All', style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(width: 8), Switch(value: showAll, onChanged: onChanged), ], ), ), OutlinedButton.icon( onPressed: onRescan, icon: const Icon(Icons.refresh), label: const Text('Rescan'), ), ], ), ], ), ); } } class _RssiBadge extends StatelessWidget { const _RssiBadge({required this.rssi}); final int rssi; @override Widget build(BuildContext context) { final color = rssi > -65 ? const Color(0xFF40C979) : rssi > -80 ? const Color(0xFFFFB649) : Theme.of(context).colorScheme.error; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(999), ), child: Text( '$rssi dBm', style: TextStyle( color: color, fontWeight: FontWeight.w700, fontSize: 12, ), ), ); } }