import 'dart:io'; import 'package:abawo_bt_app/controller/bluetooth.dart'; import 'package:abawo_bt_app/model/bluetooth_device_model.dart'; import 'package:abawo_bt_app/util/constants.dart'; import 'package:anyhow/anyhow.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:abawo_bt_app/widgets/device_listitem.dart'; import 'package:abawo_bt_app/widgets/horizontal_scanning_animation.dart'; // Import the new horizontal animation import 'package:abawo_bt_app/database/database.dart'; import 'package:drift/drift.dart' show Value; const Duration _scanDuration = Duration(seconds: 10); class ConnectDevicePage extends ConsumerStatefulWidget { const ConnectDevicePage({super.key}); @override ConsumerState createState() => _ConnectDevicePageState(); } class _ConnectDevicePageState extends ConsumerState with TickerProviderStateMixin { // TickerProviderStateMixin is no longer needed as animations are self-contained or handled by StreamBuilder int _retryScanCounter = 0; // Used to force animation reset bool _initialScanTriggered = false; // Track if the first scan was requested bool _showOnlyAbawoDevices = true; // State for filtering devices // Function to start scan safely after controller is ready void _startScanIfNeeded(BluetoothController controller) { // Use WidgetsBinding to schedule the scan start after the build phase WidgetsBinding.instance.addPostFrameCallback((_) { // Start scan only if it hasn't been triggered yet and the widget is mounted if (!_initialScanTriggered && mounted) { controller.startScan(timeout: _scanDuration); if (mounted) { setState(() { _initialScanTriggered = true; }); } } }); } @override void initState() { super.initState(); super.initState(); // No animation controllers needed here anymore } @override void dispose() { // Dispose controllers if they existed (they don't anymore) super.dispose(); } // Removed _startScanProgressAnimation, _startWaveAnimation, _stopAnimations @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Connect Device'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/'), ), actions: [ Padding( padding: const EdgeInsets.only(right: 12.0), child: Row( children: [ const Text('abawo only'), // Label for the switch Switch( value: _showOnlyAbawoDevices, onChanged: (value) { if (mounted) { setState(() { _showOnlyAbawoDevices = value; }); } }, ), ], ), ) ], ), body: Column( // Use Column instead of Center(Column(...)) children: [ const Padding( padding: EdgeInsets.all(16.0), // Add padding around the title child: Text( 'Available Devices', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), ), // Use Consumer to get the BluetoothController Expanded( // Allow the device list to take available space child: Consumer( builder: (context, ref, child) { final btAsyncValue = ref.watch(bluetoothProvider); final connectedDevices = ref.watch(nConnectedDevicesProvider).valueOrNull ?? const []; final connectedDeviceAddresses = connectedDevices .map((device) => device.deviceAddress) .toSet(); return btAsyncValue.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Center(child: Text('Error: $err')), data: (controller) { // Trigger the initial scan if needed _startScanIfNeeded(controller); // StreamBuilder for Scan Results (Device List) return StreamBuilder>( stream: controller.scanResultsStream, initialData: const [], builder: (context, snapshot) { final results = snapshot.data ?? []; // Filter results based on the toggle state final filteredResults = _showOnlyAbawoDevices ? results .where((device) => device.serviceUuids.any(isAbawoDeviceGuid)) .toList() : results; if (!_initialScanTriggered && filteredResults.isEmpty) { // Show a message or placeholder before the first scan starts or if no devices found initially return const Center( child: Text( 'Scanning for devices...')); // Or CircularProgressIndicator() } if (filteredResults.isEmpty && _initialScanTriggered) { // Show 'No devices found' only after the initial scan was triggered return const Center(child: Text('No devices found.')); } // Display the list return ListView.builder( itemCount: filteredResults.length, itemBuilder: (context, index) { final device = filteredResults[index]; final isAlreadyConnected = connectedDeviceAddresses.contains(device.id); final abawoDevice = device.serviceUuids.any(isAbawoDeviceGuid); final connectable = device.serviceUuids .any(isConnectableAbawoDeviceGuid); final deviceName = device.name.isEmpty ? 'Unknown Device' : device.name; return InkWell( onTap: () async { if (isAlreadyConnected) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'This device is already connected in the app.'), ), ); return; } if (!abawoDevice) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'This app can only connect to abawo devices.')), ); return; } else if (!connectable) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'This device is not connectable with the app.')), ); return; } else { final res = await controller.connect(device); print('res: $res'); switch (res) { case Ok(): // trigger pairing/permission prompt if needed if (!Platform.isAndroid) { controller.readCharacteristic( device.id, '0993826f-0ee4-4b37-9614-d13ecba4ffc2', '0993826f-0ee4-4b37-9614-d13ecba40000'); } // Save to DB and navigate final notifier = ref.read( nConnectedDevicesProvider.notifier); final name = device.name.isNotEmpty ? device.name : 'Unknown Device'; final deviceCompanion = ConnectedDevicesCompanion( deviceName: Value(name), deviceAddress: Value(device.id), deviceType: Value(deviceTypeToString( deviceTypeFromUuids( device.serviceUuids))), lastConnectedAt: Value(DateTime.now()), ); final addResult = await notifier .addConnectedDevice(deviceCompanion); // Check if mounted before using context if (!context.mounted) break; if (addResult.isErr()) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( content: Text( 'Failed to save device: ${addResult.unwrapErr()}')), ); } else { context.go('/device/${device.id}'); } break; case Err(:final v): ScaffoldMessenger.of(context) .showSnackBar(SnackBar( content: Text( 'Connection unsuccessful:\n${v.toString()}'), )); break; } } print('Tapped on ${device.id}'); }, child: DeviceListItem( deviceName: deviceName, deviceId: device.id, type: deviceTypeFromUuids(device.serviceUuids), ), ); }, ); }, ); }, ); }, ), ), // Bottom section: Scanning Animation and Retry Button (visible only when scanning) Consumer( // Use Consumer to get the controller for the retry button action builder: (context, ref, child) { final btController = ref .watch(bluetoothProvider) .asData ?.value; // Get controller safely return StreamBuilder( stream: btController?.isScanningStream ?? Stream.empty(), initialData: false, builder: (context, snapshot) { final isScanning = snapshot.data ?? false; // Show bottom section only if scanning if (!isScanning) { // Show only the retry button when not scanning (optional, could be hidden) // For now, let's keep the button always visible but disabled when not scannable. // A better approach might be to hide the button when not scanning. // Let's show the button but potentially disabled later if controller is null. return Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( onPressed: btController != null ? () { // Retry scan ONLY when NOT currently scanning if (mounted) { setState(() { _initialScanTriggered = true; // Ensure state reflects scan attempt _retryScanCounter++; // Increment key counter }); } btController.startScan(timeout: _scanDuration); } : null, // Disable if controller not ready child: const Text('Retry Scan'), ), ); } // If scanning, show animation and button return Container( padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16.0), decoration: BoxDecoration( color: Theme.of(context) .scaffoldBackgroundColor .withValues(alpha: 0.9), // Slight overlay effect boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, -4), // Shadow upwards ) ], ), child: Column( mainAxisSize: MainAxisSize.min, // Keep column compact children: [ // Pass isScanning and the ValueKey HorizontalScanningAnimation( key: ValueKey( _retryScanCounter), // Force state rebuild on counter change isScanning: isScanning, height: 40, ), const SizedBox(height: 8), ElevatedButton( // Button does nothing if pressed *while* scanning. // It just indicates the status. onPressed: null, // Disable button while scanning style: ElevatedButton.styleFrom( disabledBackgroundColor: Theme.of(context) .primaryColor .withValues(alpha: 0.5), // Custom disabled color disabledForegroundColor: Colors.white70, ), child: const Text('Scanning...'), // Just indicate status ), const SizedBox(height: 8), // Add some bottom padding ], ), ); }, ); }), ], // End of outer Column children ), // End of Scaffold ); } }