feat: working connection, conn setting, and gear ratio setting for universal shifters
This commit is contained in:
@ -1,11 +1,17 @@
|
||||
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_blue_plus/flutter_blue_plus.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/scanning_animation.dart'; // Import the new animation widget
|
||||
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);
|
||||
|
||||
@ -18,25 +24,20 @@ class ConnectDevicePage extends ConsumerStatefulWidget {
|
||||
|
||||
class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
||||
with TickerProviderStateMixin {
|
||||
// Use TickerProviderStateMixin for multiple controllers if needed later, good practice
|
||||
bool _initialScanStarted = false;
|
||||
// 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
|
||||
late AnimationController
|
||||
_progressController; // Controller for scan duration progress
|
||||
late AnimationController
|
||||
_waveAnimationController; // Controller for wave animation
|
||||
|
||||
// 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((_) {
|
||||
if (!_initialScanStarted && mounted) {
|
||||
// Start scan only if it hasn't been triggered yet and the widget is mounted
|
||||
if (!_initialScanTriggered && mounted) {
|
||||
controller.startScan(timeout: _scanDuration);
|
||||
_startScanProgressAnimation(); // Start scan duration progress animation
|
||||
_startWaveAnimation(); // Start the wave animation
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_initialScanStarted = true;
|
||||
_initialScanTriggered = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -46,64 +47,17 @@ class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize scan progress controller
|
||||
_progressController = AnimationController(
|
||||
vsync: this,
|
||||
duration: _scanDuration,
|
||||
)..addListener(() {
|
||||
// Trigger rebuild when animation value changes for the progress indicator
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize wave animation controller
|
||||
_waveAnimationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1500), // New duration: 1.5 seconds
|
||||
)..repeat(); // Make the wave animation repeat
|
||||
super.initState();
|
||||
// No animation controllers needed here anymore
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Stop animations before disposing
|
||||
_progressController.stop();
|
||||
_waveAnimationController.stop();
|
||||
_progressController.dispose();
|
||||
_waveAnimationController.dispose();
|
||||
// Dispose controllers if they existed (they don't anymore)
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Helper method to start/reset scan progress animation
|
||||
void _startScanProgressAnimation() {
|
||||
if (mounted) {
|
||||
_progressController.reset();
|
||||
_progressController.forward().whenCompleteOrCancel(() {
|
||||
// Optional: Add logic if needed when scan progress completes or cancels
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to start the wave animation
|
||||
void _startWaveAnimation() {
|
||||
if (mounted && !_waveAnimationController.isAnimating) {
|
||||
_waveAnimationController.reset();
|
||||
_waveAnimationController.repeat();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to stop animations when scan finishes/cancels
|
||||
void _stopAnimations() {
|
||||
if (mounted) {
|
||||
if (_progressController.isAnimating) {
|
||||
_progressController.stop();
|
||||
}
|
||||
if (_waveAnimationController.isAnimating) {
|
||||
_waveAnimationController.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removed _startScanProgressAnimation, _startWaveAnimation, _stopAnimations
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -134,155 +88,264 @@ class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
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),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Use Consumer to react to bluetoothProvider changes
|
||||
Expanded(
|
||||
// Allow the Consumer content to expand
|
||||
child: Consumer(builder: (context, ref, child) {
|
||||
),
|
||||
// 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 <ConnectedDevice>[];
|
||||
final connectedDeviceAddresses = connectedDevices
|
||||
.map((device) => device.deviceAddress)
|
||||
.toSet();
|
||||
|
||||
return btAsyncValue.when(
|
||||
loading: () => const Center(
|
||||
child:
|
||||
CircularProgressIndicator()), // Center loading indicator
|
||||
error: (err, stack) => Center(
|
||||
child: Text(
|
||||
'Error loading Bluetooth: $err')), // Center error
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
data: (controller) {
|
||||
// Start the initial scan once the controller is ready
|
||||
// Start initial scan and animation
|
||||
// Trigger the initial scan if needed
|
||||
_startScanIfNeeded(controller);
|
||||
|
||||
// Use StreamBuilder to watch the scanning state
|
||||
return StreamBuilder<bool>(
|
||||
stream: FlutterBluePlus.isScanning,
|
||||
initialData:
|
||||
false, // Default to not scanning before check
|
||||
// StreamBuilder for Scan Results (Device List)
|
||||
return StreamBuilder<List<DiscoveredDevice>>(
|
||||
stream: controller.scanResultsStream,
|
||||
initialData: const [],
|
||||
builder: (context, snapshot) {
|
||||
final isScanning = snapshot.data ?? false;
|
||||
final results = snapshot.data ?? [];
|
||||
// Filter results based on the toggle state
|
||||
final filteredResults = _showOnlyAbawoDevices
|
||||
? results
|
||||
.where((device) =>
|
||||
device.serviceUuids.any(isAbawoDeviceGuid))
|
||||
.toList()
|
||||
: results;
|
||||
|
||||
if (isScanning && _initialScanStarted) {
|
||||
_startWaveAnimation(); // Ensure wave animation is running
|
||||
// Show the new scanning wave animation with the progress indicator value
|
||||
return Center(
|
||||
// Pass the wave animation controller. The progress value will be added
|
||||
// to the ScanningWaveAnimation widget itself later.
|
||||
child: ScanningWaveAnimation(
|
||||
animation: _waveAnimationController,
|
||||
progressValue: _progressController
|
||||
.value, // Pass the scan progress
|
||||
),
|
||||
);
|
||||
} else if (!_initialScanStarted) {
|
||||
// Show placeholder or button to start initial scan if needed, or just empty space
|
||||
return const SizedBox(
|
||||
height: 50); // Placeholder before scan starts
|
||||
} else {
|
||||
// Scan finished, stop animations and show results
|
||||
_stopAnimations();
|
||||
final results = controller.scanResults;
|
||||
// Filter results based on the toggle state
|
||||
final filteredResults = _showOnlyAbawoDevices
|
||||
? results
|
||||
.where((device) => device
|
||||
.advertisementData.serviceUuids
|
||||
.contains(Guid(abawoServiceBtUUID)))
|
||||
.toList()
|
||||
: results;
|
||||
|
||||
// Use Column + Expanded for ListView + Button layout
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
// Allow ListView to take available space
|
||||
child: filteredResults
|
||||
.isEmpty // Use filtered list check
|
||||
? const Center(
|
||||
child: Text(
|
||||
'No devices found.')) // Center empty text
|
||||
: ListView.builder(
|
||||
itemCount: filteredResults
|
||||
.length, // Use filtered list length
|
||||
itemBuilder: (context, index) {
|
||||
final device = filteredResults[
|
||||
index]; // Use filtered list
|
||||
final isAbawoDevice = device
|
||||
.advertisementData.serviceUuids
|
||||
.contains(
|
||||
Guid(abawoServiceBtUUID));
|
||||
final deviceName =
|
||||
device.device.advName.isEmpty
|
||||
? 'Unknown Device'
|
||||
: device.device.advName;
|
||||
// Use the custom DeviceListItem widget
|
||||
return InkWell(
|
||||
// Wrap with InkWell for tap feedback
|
||||
onTap: () {
|
||||
if (!isAbawoDevice) {
|
||||
// Show a snackbar for non-Abawo devices
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'This app can only connect to abawo devices.')));
|
||||
return;
|
||||
}
|
||||
// TODO: Implement connect logic
|
||||
// controller.connectToDevice(device.device); // Pass the BluetoothDevice
|
||||
// context.go('/control/${device.device.remoteId.str}');
|
||||
print(
|
||||
'Tapped on ${device.device.remoteId.str}');
|
||||
},
|
||||
child: DeviceListItem(
|
||||
deviceName: deviceName,
|
||||
deviceId:
|
||||
device.device.remoteId.str,
|
||||
isUnknownDevice:
|
||||
device.device.advName.isEmpty,
|
||||
),
|
||||
);
|
||||
} // End of itemBuilder
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
// Add padding around the button
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Retry scan by calling startScan on the controller
|
||||
// Ensure _initialScanStarted is true so indicator shows
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_initialScanStarted = true;
|
||||
});
|
||||
}
|
||||
controller.startScan(
|
||||
timeout: _scanDuration);
|
||||
_startScanProgressAnimation(); // Restart scan progress animation
|
||||
_startWaveAnimation(); // Ensure wave animation runs on retry
|
||||
},
|
||||
child: const Text('Retry Scan'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
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<bool>(
|
||||
stream: btController?.isScanningStream ?? Stream<bool>.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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user