feat: working connection, conn setting, and gear ratio setting for universal shifters

This commit is contained in:
2026-02-22 23:05:12 +01:00
parent f92d6d04f5
commit dcb1e6596e
93 changed files with 10538 additions and 668 deletions

View File

@ -0,0 +1,214 @@
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<DiscoveredDevice?> show(
BuildContext context, {
required String excludedDeviceId,
}) {
return showDialog<DiscoveredDevice>(
context: context,
barrierDismissible: true,
builder: (_) => BikeScanDialog(excludedDeviceId: excludedDeviceId),
);
}
@override
ConsumerState<BikeScanDialog> createState() => _BikeScanDialogState();
}
class _BikeScanDialogState extends ConsumerState<BikeScanDialog> {
bool _showAll = false;
BluetoothController? _controller;
@override
void initState() {
super.initState();
_startScan();
}
Future<void> _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);
return Dialog(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: SizedBox(
width: 520,
height: 520,
child: btAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(child: Text('Bluetooth error: $err')),
data: (controller) {
_controller ??= controller;
return Column(
children: [
_buildHeader(context),
const Divider(height: 1),
Expanded(
child: StreamBuilder<List<DiscoveredDevice>>(
stream: controller.scanResultsStream,
initialData: controller.scanResults,
builder: (context, snapshot) {
final devices =
_filteredDevices(snapshot.data ?? const []);
if (devices.isEmpty) {
return const Center(
child: Text('No matching devices nearby.'),
);
}
return ListView.separated(
itemCount: devices.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final device = devices[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.colorScheme
.primaryContainer,
child: const Icon(Icons.pedal_bike),
),
title: Text(
device.name.isEmpty
? 'Unknown Device'
: device.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
device.id,
style: const TextStyle(fontFamily: 'monospace'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: _RssiBadge(rssi: device.rssi),
onTap: () {
Navigator.of(context).pop(device);
},
);
},
);
},
),
),
],
);
},
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 12, 12),
child: Row(
children: [
const Expanded(
child: Text(
'Select Bike',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
),
),
Row(
children: [
const Text('Show All'),
Switch(
value: _showAll,
onChanged: (value) {
setState(() {
_showAll = value;
});
},
),
],
),
IconButton(
tooltip: 'Rescan',
onPressed: _startScan,
icon: const Icon(Icons.refresh),
),
IconButton(
tooltip: 'Close',
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
),
],
),
);
}
List<DiscoveredDevice> _filteredDevices(List<DiscoveredDevice> 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 _RssiBadge extends StatelessWidget {
const _RssiBadge({required this.rssi});
final int rssi;
@override
Widget build(BuildContext context) {
final color = rssi > -65
? Colors.green
: rssi > -80
? Colors.orange
: Colors.red;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$rssi dBm',
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
);
}
}