import 'package:abawo_bt_app/controller/bluetooth.dart'; import 'package:abawo_bt_app/controller/shifter_device_telemetry.dart'; import 'package:abawo_bt_app/database/database.dart'; import 'package:abawo_bt_app/model/bluetooth_device_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; class DevicesTabPage extends ConsumerWidget { const DevicesTabPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final devicesAsync = ref.watch(nConnectedDevicesProvider); final connectionData = ref.watch(connectionStatusProvider).valueOrNull; return ListView( padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Devices', style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), Text( 'Manage connected hardware and jump back into setup.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.68), ), ), ], ), ), IconButton.filledTonal( tooltip: 'Connect a device', onPressed: () => context.push('/connect_device'), icon: const Icon(Icons.add), ), ], ), const SizedBox(height: 20), devicesAsync.when( loading: () => const _LoadingCard(), error: (error, _) => _MessageCard( title: 'Could not load devices', message: error.toString(), ), data: (devices) => _ActiveDeviceCard( devices: devices, connectionData: connectionData, ), ), const SizedBox(height: 20), Row( children: [ Expanded( child: Text( 'My Devices', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), ), ), TextButton.icon( onPressed: () => context.push('/connect_device'), icon: const Icon(Icons.add_circle_outline), label: const Text('Add Device'), ), ], ), const SizedBox(height: 10), const _SavedDevicesList(), ], ); } } class _SavedDevicesList extends ConsumerStatefulWidget { const _SavedDevicesList(); @override ConsumerState<_SavedDevicesList> createState() => _SavedDevicesListState(); } class _SavedDevicesListState extends ConsumerState<_SavedDevicesList> { String? _connectingDeviceId; Future _removeDevice(ConnectedDevice device) async { final shouldRemove = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Remove device?'), content: Text('Do you want to remove ${device.deviceName} from the app?'), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('Remove'), ), ], ), ); if (shouldRemove != true || !mounted) { return; } final result = await ref .read(nConnectedDevicesProvider.notifier) .deleteConnectedDevice(device.id); if (!mounted) { return; } if (result.isErr()) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to remove device: ${result.unwrapErr()}'), ), ); return; } if (_connectingDeviceId == device.deviceAddress) { setState(() { _connectingDeviceId = null; }); } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${device.deviceName} removed from the app.')), ); } Future _openDevice(ConnectedDevice device) async { if (_connectingDeviceId != null) { return; } setState(() { _connectingDeviceId = device.deviceAddress; }); try { final controller = await ref.read(bluetoothProvider.future); final result = await controller.connectById( device.deviceAddress, timeout: const Duration(seconds: 10), ); if (!mounted) { return; } if (result.isOk()) { await ref .read(nConnectedDevicesProvider.notifier) .updateConnectedDeviceLastConnected(device.id); if (!mounted) { return; } context.push('/device/${device.deviceAddress}'); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Connection failed. Is the device turned on and in range?', ), duration: Duration(seconds: 3), ), ); } } catch (error) { if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $error')), ); } finally { if (mounted) { setState(() { _connectingDeviceId = null; }); } } } @override Widget build(BuildContext context) { final devicesAsync = ref.watch(nConnectedDevicesProvider); final connectionData = ref.watch(connectionStatusProvider).valueOrNull; final connectedDeviceId = connectionData?.$2; final connectionState = connectionData?.$1; return devicesAsync.when( loading: () => const _LoadingCard(), error: (error, _) => _MessageCard( title: 'Could not load saved devices', message: error.toString(), ), data: (devices) { if (devices.isEmpty) { return _MessageCard( title: 'No devices yet', message: 'Add your first shifter to start configuring it.', actionLabel: 'Connect Device', onAction: () => context.push('/connect_device'), ); } return Column( children: [ for (final device in devices) ...[ _SavedDeviceTile( device: device, isConnecting: device.deviceAddress == _connectingDeviceId, isConnected: connectedDeviceId == device.deviceAddress && connectionState == ConnectionStatus.connected, onTap: () => _openDevice(device), onRemove: () => _removeDevice(device), ), if (device != devices.last) const SizedBox(height: 12), ], const SizedBox(height: 12), OutlinedButton.icon( onPressed: () => context.push('/connect_device'), icon: const Icon(Icons.bluetooth_searching), label: const Text('Connect Device'), ), ], ); }, ); } } class _ActiveDeviceCard extends ConsumerWidget { const _ActiveDeviceCard({ required this.devices, required this.connectionData, }); final List devices; final (ConnectionStatus, String?)? connectionData; @override Widget build(BuildContext context, WidgetRef ref) { final shifterDevices = devices .where( (device) => deviceTypeFromString(device.deviceType) == DeviceType.universalShifters, ) .toList() ..sort((a, b) { final aLastConnected = a.lastConnectedAt ?? a.createdAt; final bLastConnected = b.lastConnectedAt ?? b.createdAt; return bLastConnected.compareTo(aLastConnected); }); if (shifterDevices.isEmpty) { return const SizedBox.shrink(); } final connectedId = connectionData?.$2; final primaryDevice = connectedId == null ? shifterDevices.first : shifterDevices.firstWhere( (device) => device.deviceAddress == connectedId, orElse: () => shifterDevices.first, ); final isConnected = connectedId == primaryDevice.deviceAddress && connectionData?.$1 == ConnectionStatus.connected; final telemetry = ref.watch( shifterDeviceTelemetryCacheProvider.select( (cache) => cache[primaryDevice.deviceAddress], ), ); final batteryLabel = telemetry?.batteryLabel ?? '--'; const signalLabel = 'Ready'; final firmwareLabel = telemetry?.firmwareLabel ?? '--'; return Card( child: Padding( padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: BorderRadius.circular(18), child: Image.asset( 'assets/images/shifter-wireframe.png', width: 96, height: 72, fit: BoxFit.cover, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( primaryDevice.deviceName, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), _StatusChip( label: isConnected ? 'Connected' : 'Saved device', color: isConnected ? const Color(0xFF40C979) : Theme.of(context).colorScheme.primary, ), const SizedBox(height: 10), Text( primaryDevice.deviceAddress, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.62), ), ), ], ), ), ], ), const SizedBox(height: 18), Row( children: [ Expanded( child: _MetricTile( label: 'Battery', value: batteryLabel, icon: Icons.battery_charging_full_rounded, ), ), const SizedBox(width: 10), const Expanded( child: _MetricTile( label: 'Signal', value: signalLabel, icon: Icons.signal_cellular_alt, ), ), const SizedBox(width: 10), Expanded( child: _MetricTile( label: 'Firmware', value: firmwareLabel, icon: Icons.memory_rounded, ), ), ], ), ], ), ), ); } } class _SavedDeviceTile extends StatelessWidget { const _SavedDeviceTile({ required this.device, required this.isConnecting, required this.isConnected, required this.onTap, required this.onRemove, }); final ConnectedDevice device; final bool isConnecting; final bool isConnected; final VoidCallback onTap; final VoidCallback onRemove; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Material( color: colorScheme.surface, borderRadius: BorderRadius.circular(22), child: InkWell( borderRadius: BorderRadius.circular(22), onTap: isConnecting ? null : onTap, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(22), border: Border.all( color: isConnected ? colorScheme.primary.withValues(alpha: 0.65) : colorScheme.outlineVariant.withValues(alpha: 0.55), ), ), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( shape: BoxShape.circle, color: isConnected ? colorScheme.primary.withValues(alpha: 0.14) : colorScheme.surfaceContainerHighest .withValues(alpha: 0.7), ), child: Icon( deviceTypeFromString(device.deviceType) == DeviceType.universalShifters ? Icons.bluetooth_rounded : Icons.memory_rounded, color: isConnected ? colorScheme.primary : colorScheme.onSurface, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( device.deviceName, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), Text( isConnected ? 'Connected' : 'Saved device', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isConnected ? const Color(0xFF40C979) : colorScheme.primary, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( device.deviceAddress, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.62), ), ), ], ), ), if (isConnecting) const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2.4), ) else Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( tooltip: 'Remove device', onPressed: onRemove, icon: const Icon(Icons.delete_outline), ), Icon( Icons.chevron_right_rounded, color: colorScheme.onSurface.withValues(alpha: 0.55), ), ], ), ], ), ), ), ); } } class _MetricTile extends StatelessWidget { const _MetricTile({ required this.label, required this.value, required this.icon, }); final String label; final String value; final IconData icon; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.54), borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 18, color: colorScheme.primary), const SizedBox(height: 10), Text( label, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.62), ), ), const SizedBox(height: 2), Text( value, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), ], ), ); } } class _MessageCard extends StatelessWidget { const _MessageCard({ required this.title, required this.message, this.actionLabel, this.onAction, }); final String title; final String message; final String? actionLabel; final VoidCallback? onAction; @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( message, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.68), ), ), if (actionLabel != null && onAction != null) ...[ const SizedBox(height: 14), FilledButton.icon( onPressed: onAction, icon: const Icon(Icons.add_circle_outline), label: Text(actionLabel!), ), ], ], ), ), ); } } class _LoadingCard extends StatelessWidget { const _LoadingCard(); @override Widget build(BuildContext context) { return const Card( child: Padding( padding: EdgeInsets.all(28), child: Center(child: CircularProgressIndicator()), ), ); } } class _StatusChip extends StatelessWidget { const _StatusChip({ required this.label, required this.color, }); final String label; final Color color; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(999), border: Border.all(color: color.withValues(alpha: 0.24)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.check_circle_outline, size: 14, color: color), const SizedBox(width: 6), Text( label, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: color, fontWeight: FontWeight.w700, ), ), ], ), ); } }