refactoring #1

This commit is contained in:
Jonas Bark
2025-10-26 16:26:56 +01:00
parent 75eef49317
commit dd73c3249b
11 changed files with 258 additions and 223 deletions

View File

@@ -5,7 +5,8 @@ import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:gamepads/gamepads.dart';
import 'package:swift_control/bluetooth/devices/gamepad/gamepad.dart';
import 'package:swift_control/bluetooth/devices/bluetooth_device.dart';
import 'package:swift_control/bluetooth/devices/gamepad/gamepad_device.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/android.dart';
import 'package:swift_control/utils/requirements/android.dart';
@@ -17,9 +18,13 @@ import 'messages/notification.dart';
class Connection {
final devices = <BaseDevice>[];
List<BluetoothDevice> get bluetoothDevices => devices.whereType<BluetoothDevice>().toList();
List<GamepadDevice> get gamepadDevices => devices.whereType<GamepadDevice>().toList();
var _androidNotificationsSetup = false;
final _connectionQueue = <BaseDevice>[];
final _connectionQueue = <BluetoothDevice>[];
var _handlingConnectionQueue = false;
final Map<BaseDevice, StreamSubscription<BaseNotification>> _streamSubscriptions = {};
@@ -51,10 +56,11 @@ class Connection {
print('Scan result: ${result.name} - ${result.deviceId}');
}
final scanResult = BaseDevice.fromScanResult(result);
final scanResult = BluetoothDevice.fromScanResult(result);
if (scanResult != null) {
_actionStreams.add(LogNotification('Found new device: ${scanResult.runtimeType}'));
_connectionQueue.addAll([scanResult]);
_addDevices([scanResult]);
} else {
final manufacturerData = result.manufacturerDataList;
@@ -69,7 +75,7 @@ class Connection {
};
UniversalBle.onValueChange = (deviceId, characteristicUuid, value) {
final device = devices.firstOrNullWhere((e) => e.device.deviceId == deviceId);
final device = bluetoothDevices.firstOrNullWhere((e) => e.device.deviceId == deviceId);
if (device == null) {
_actionStreams.add(LogNotification('Device not found: $deviceId'));
UniversalBle.disconnect(deviceId);
@@ -80,27 +86,17 @@ class Connection {
};
Gamepads.list().then((list) {
print('Connected gamepads: ${list.length}');
if (list.isNotEmpty) {
final pads = list
.map(
(pad) => Gamepad(
BleDevice(
deviceId: pad.id,
name: pad.name,
rssi: 0,
services: [],
),
),
(pad) => GamepadDevice(pad.name, id: pad.id),
)
.toList();
_addDevices(pads);
Gamepads.events.listen((event) {
_actionStreams.add(LogNotification('Gamepad event: $event'));
final device = devices.firstOrNullWhere((e) => e.device.deviceId == event.gamepadId);
if (device is Gamepad) {
device.processGamepadEvent(event);
}
final device = gamepadDevices.firstOrNullWhere((e) => e.id == event.gamepadId);
device?.processGamepadEvent(event);
});
}
});
@@ -117,10 +113,11 @@ class Connection {
// does not work on web, may not work on Windows
if (!kIsWeb && !Platform.isWindows) {
UniversalBle.getSystemDevices(
withServices: BaseDevice.servicesToScan,
withServices: BluetoothDevice.servicesToScan,
).then((devices) async {
final baseDevices = devices.mapNotNull(BaseDevice.fromScanResult).toList();
final baseDevices = devices.mapNotNull(BluetoothDevice.fromScanResult).toList();
if (baseDevices.isNotEmpty) {
_connectionQueue.addAll(baseDevices);
_addDevices(baseDevices);
}
});
@@ -129,15 +126,13 @@ class Connection {
await UniversalBle.startScan(
// allow all to enable Wahoo Kickr Bike Shift detection
//scanFilter: ScanFilter(withServices: BaseDevice.servicesToScan),
platformConfig: PlatformConfig(web: WebOptions(optionalServices: BaseDevice.servicesToScan)),
platformConfig: PlatformConfig(web: WebOptions(optionalServices: BluetoothDevice.servicesToScan)),
);
}
void _addDevices(List<BaseDevice> dev) {
final newDevices = dev.where((device) => !devices.contains(device)).toList();
devices.addAll(newDevices);
_connectionQueue.addAll(newDevices);
_handleConnectionQueue();
hasDevices.value = devices.isNotEmpty;
@@ -154,30 +149,28 @@ class Connection {
if (_connectionQueue.isNotEmpty && !_handlingConnectionQueue) {
_handlingConnectionQueue = true;
final device = _connectionQueue.removeAt(0);
if (device is! Gamepad) {
_actionStreams.add(LogNotification('Connecting to: ${device.device.name ?? device.runtimeType}'));
_connect(device)
.then((_) {
_handlingConnectionQueue = false;
_actionStreams.add(LogNotification('Connection finished: ${device.device.name ?? device.runtimeType}'));
if (_connectionQueue.isNotEmpty) {
_handleConnectionQueue();
}
})
.catchError((e) {
_handlingConnectionQueue = false;
_actionStreams.add(
LogNotification('Connection failed: ${device.device.name ?? device.runtimeType} - $e'),
);
if (_connectionQueue.isNotEmpty) {
_handleConnectionQueue();
}
});
}
_actionStreams.add(LogNotification('Connecting to: ${device.device.name ?? device.runtimeType}'));
_connect(device)
.then((_) {
_handlingConnectionQueue = false;
_actionStreams.add(LogNotification('Connection finished: ${device.device.name ?? device.runtimeType}'));
if (_connectionQueue.isNotEmpty) {
_handleConnectionQueue();
}
})
.catchError((e) {
_handlingConnectionQueue = false;
_actionStreams.add(
LogNotification('Connection failed: ${device.device.name ?? device.runtimeType} - $e'),
);
if (_connectionQueue.isNotEmpty) {
_handleConnectionQueue();
}
});
}
}
Future<void> _connect(BaseDevice bleDevice) async {
Future<void> _connect(BluetoothDevice bleDevice) async {
try {
final actionSubscription = bleDevice.actionStream.listen((data) {
_actionStreams.add(data);
@@ -219,7 +212,7 @@ class Connection {
}
UniversalBle.stopScan();
isScanning.value = false;
for (var device in devices) {
for (var device in bluetoothDevices) {
_streamSubscriptions[device]?.cancel();
_streamSubscriptions.remove(device);
_connectionSubscriptions[device]?.cancel();

View File

@@ -1,152 +1,41 @@
import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:swift_control/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart';
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_click.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_clickv2.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_play.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/desktop.dart';
import 'package:universal_ble/universal_ble.dart';
import '../../utils/keymap/buttons.dart';
import '../messages/notification.dart';
import 'elite/elite_square.dart';
import 'elite/elite_sterzo.dart';
abstract class BaseDevice {
final BleDevice scanResult;
final String name;
final bool isBeta;
final List<ControllerButton> availableButtons;
BaseDevice(this.scanResult, {required this.availableButtons, this.isBeta = false});
BaseDevice(this.name, {required this.availableButtons, this.isBeta = false});
bool isConnected = false;
int? batteryLevel;
String? firmwareVersion;
Timer? _longPressTimer;
Set<ControllerButton> _previouslyPressedButtons = <ControllerButton>{};
static List<String> servicesToScan = [
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID,
ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID,
SquareConstants.SERVICE_UUID,
WahooKickrBikeShiftConstants.SERVICE_UUID,
SterzoConstants.SERVICE_UUID,
];
static BaseDevice? fromScanResult(BleDevice scanResult) {
// Use the name first as the "System Devices" and Web (android sometimes Windows) don't have manufacturer data
BaseDevice? device;
if (kIsWeb) {
device = switch (scanResult.name) {
'Zwift Ride' => ZwiftRide(scanResult),
'Zwift Play' => ZwiftPlay(scanResult),
'Zwift Click' => ZwiftClickV2(scanResult),
'SQUARE' => EliteSquare(scanResult),
_ => null,
};
if (scanResult.name != null && scanResult.name!.toUpperCase().startsWith('KICKR BIKE SHIFT')) {
device = WahooKickrBikeShift(scanResult);
}
if (scanResult.name != null && scanResult.name!.toUpperCase().startsWith('STERZO')) {
device = EliteSterzo(scanResult);
}
} else {
device = switch (scanResult.name) {
//'Zwift Ride' => ZwiftRide(scanResult), special case for Zwift Ride: we must only connect to the left controller
// https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/
'Zwift Play' => ZwiftPlay(scanResult),
//'Zwift Click' => ZwiftClick(scanResult), special case for Zwift Click v2: we must only connect to the left controller
_ => null,
};
if (scanResult.name != null) {
if (scanResult.name!.toUpperCase().startsWith('STERZO')) {
device = EliteSterzo(scanResult);
} else if (scanResult.name!.toUpperCase().startsWith('KICKR BIKE SHIFT')) {
return WahooKickrBikeShift(scanResult);
}
}
}
if (device != null) {
return device;
} else if (scanResult.services.containsAny([
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID,
ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID,
])) {
// otherwise use the manufacturer data to identify the device
final manufacturerData = scanResult.manufacturerDataList;
final data = manufacturerData
.firstOrNullWhere((e) => e.companyId == ZwiftConstants.ZWIFT_MANUFACTURER_ID)
?.payload;
if (data == null || data.isEmpty) {
return null;
}
final type = ZwiftDeviceType.fromManufacturerData(data.first);
return switch (type) {
ZwiftDeviceType.click => ZwiftClick(scanResult),
ZwiftDeviceType.playRight => ZwiftPlay(scanResult),
ZwiftDeviceType.playLeft => ZwiftPlay(scanResult),
ZwiftDeviceType.rideLeft => ZwiftRide(scanResult),
//DeviceType.rideRight => ZwiftRide(scanResult), // see comment above
ZwiftDeviceType.clickV2Left => ZwiftClickV2(scanResult),
//DeviceType.clickV2Right => ZwiftClickV2(scanResult), // see comment above
_ => null,
};
} else if (scanResult.services.contains(SquareConstants.SERVICE_UUID)) {
return EliteSquare(scanResult);
} else if (scanResult.services.contains(SterzoConstants.SERVICE_UUID)) {
return EliteSterzo(scanResult);
} else {
return null;
}
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BaseDevice && runtimeType == other.runtimeType && scanResult == other.scanResult;
identical(this, other) || other is BaseDevice && runtimeType == other.runtimeType && name == other.name;
@override
int get hashCode => scanResult.hashCode;
int get hashCode => name.hashCode;
@override
String toString() {
return runtimeType.toString();
}
BleDevice get device => scanResult!;
final StreamController<BaseNotification> actionStreamInternal = StreamController<BaseNotification>.broadcast();
Stream<BaseNotification> get actionStream => actionStreamInternal.stream;
Future<void> connect() async {
actionStream.listen((message) {
print("Received message: $message");
});
await UniversalBle.connect(device.deviceId);
if (!kIsWeb) {
await UniversalBle.requestMtu(device.deviceId, 517);
}
final services = await UniversalBle.discoverServices(device.deviceId);
await handleServices(services);
}
Future<void> handleServices(List<BleService> services);
Future<void> processCharacteristic(String characteristic, Uint8List bytes);
Future<void> connect();
Future<void> handleButtonsClicked(List<ControllerButton>? buttonsClicked) async {
if (buttonsClicked == null) {
@@ -232,7 +121,8 @@ abstract class BaseDevice {
await (actionHandler as DesktopActions).releaseAllHeldKeys(_previouslyPressedButtons.toList());
}
_previouslyPressedButtons.clear();
await UniversalBle.disconnect(device.deviceId);
isConnected = false;
}
Widget showInformation(BuildContext context);
}

View File

@@ -0,0 +1,185 @@
import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart';
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_click.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_clickv2.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_play.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart';
import 'package:swift_control/pages/device.dart';
import 'package:swift_control/widgets/beta_pill.dart';
import 'package:universal_ble/universal_ble.dart';
import 'elite/elite_square.dart';
import 'elite/elite_sterzo.dart';
abstract class BluetoothDevice extends BaseDevice {
final BleDevice scanResult;
BluetoothDevice(this.scanResult, {required super.availableButtons, super.isBeta = false})
: super(scanResult.name ?? 'Unknown Device');
int? batteryLevel;
String? firmwareVersion;
static List<String> servicesToScan = [
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID,
ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID,
SquareConstants.SERVICE_UUID,
WahooKickrBikeShiftConstants.SERVICE_UUID,
SterzoConstants.SERVICE_UUID,
];
static BluetoothDevice? fromScanResult(BleDevice scanResult) {
// Use the name first as the "System Devices" and Web (android sometimes Windows) don't have manufacturer data
BluetoothDevice? device;
if (kIsWeb) {
device = switch (scanResult.name) {
'Zwift Ride' => ZwiftRide(scanResult),
'Zwift Play' => ZwiftPlay(scanResult),
'Zwift Click' => ZwiftClickV2(scanResult),
'SQUARE' => EliteSquare(scanResult),
_ => null,
};
if (scanResult.name != null && scanResult.name!.toUpperCase().startsWith('KICKR BIKE SHIFT')) {
device = WahooKickrBikeShift(scanResult);
}
if (scanResult.name != null && scanResult.name!.toUpperCase().startsWith('STERZO')) {
device = EliteSterzo(scanResult);
}
} else {
device = switch (scanResult.name) {
//'Zwift Ride' => ZwiftRide(scanResult), special case for Zwift Ride: we must only connect to the left controller
// https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/
'Zwift Play' => ZwiftPlay(scanResult),
//'Zwift Click' => ZwiftClick(scanResult), special case for Zwift Click v2: we must only connect to the left controller
_ => null,
};
if (scanResult.name != null) {
if (scanResult.name!.toUpperCase().startsWith('STERZO')) {
device = EliteSterzo(scanResult);
} else if (scanResult.name!.toUpperCase().startsWith('KICKR BIKE SHIFT')) {
return WahooKickrBikeShift(scanResult);
}
}
}
if (device != null) {
return device;
} else if (scanResult.services.containsAny([
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID,
ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID,
])) {
// otherwise use the manufacturer data to identify the device
final manufacturerData = scanResult.manufacturerDataList;
final data = manufacturerData
.firstOrNullWhere((e) => e.companyId == ZwiftConstants.ZWIFT_MANUFACTURER_ID)
?.payload;
if (data == null || data.isEmpty) {
return null;
}
final type = ZwiftDeviceType.fromManufacturerData(data.first);
return switch (type) {
ZwiftDeviceType.click => ZwiftClick(scanResult),
ZwiftDeviceType.playRight => ZwiftPlay(scanResult),
ZwiftDeviceType.playLeft => ZwiftPlay(scanResult),
ZwiftDeviceType.rideLeft => ZwiftRide(scanResult),
//DeviceType.rideRight => ZwiftRide(scanResult), // see comment above
ZwiftDeviceType.clickV2Left => ZwiftClickV2(scanResult),
//DeviceType.clickV2Right => ZwiftClickV2(scanResult), // see comment above
_ => null,
};
} else if (scanResult.services.contains(SquareConstants.SERVICE_UUID)) {
return EliteSquare(scanResult);
} else if (scanResult.services.contains(SterzoConstants.SERVICE_UUID)) {
return EliteSterzo(scanResult);
} else {
return null;
}
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BluetoothDevice && runtimeType == other.runtimeType && scanResult == other.scanResult;
@override
int get hashCode => scanResult.hashCode;
@override
String toString() {
return runtimeType.toString();
}
BleDevice get device => scanResult;
@override
Future<void> connect() async {
actionStream.listen((message) {
print("Received message: $message");
});
await UniversalBle.connect(device.deviceId);
if (!kIsWeb) {
await UniversalBle.requestMtu(device.deviceId, 517);
}
final services = await UniversalBle.discoverServices(device.deviceId);
await handleServices(services);
}
Future<void> handleServices(List<BleService> services);
Future<void> processCharacteristic(String characteristic, Uint8List bytes);
@override
Future<void> disconnect() async {
await UniversalBle.disconnect(device.deviceId);
super.disconnect();
}
@override
Widget showInformation(BuildContext context) {
return Row(
children: [
Text(
device.name?.screenshot ?? device.runtimeType.toString(),
style: TextStyle(fontWeight: FontWeight.bold),
),
if (isBeta) BetaPill(),
if (batteryLevel != null) ...[
Icon(switch (batteryLevel!) {
>= 80 => Icons.battery_full,
>= 60 => Icons.battery_6_bar,
>= 50 => Icons.battery_5_bar,
>= 25 => Icons.battery_4_bar,
>= 10 => Icons.battery_2_bar,
_ => Icons.battery_alert,
}),
Text('$batteryLevel%'),
if (firmwareVersion != null) Text(' - Firmware: $firmwareVersion'),
if (firmwareVersion != null &&
this is ZwiftDevice &&
firmwareVersion != (this as ZwiftDevice).latestFirmwareVersion) ...[
SizedBox(width: 8),
Icon(Icons.warning, color: Theme.of(context).colorScheme.error),
Text(
' (latest: ${(this as ZwiftDevice).latestFirmwareVersion})',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
],
],
);
}
}

View File

@@ -1,13 +1,13 @@
import 'dart:typed_data';
import 'package:dartx/dartx.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:universal_ble/universal_ble.dart';
import '../../messages/notification.dart';
import '../bluetooth_device.dart';
class EliteSquare extends BaseDevice {
class EliteSquare extends BluetoothDevice {
EliteSquare(super.scanResult)
: super(
availableButtons: SquareConstants.BUTTON_MAPPING.values.toList(),

View File

@@ -4,13 +4,13 @@ import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:universal_ble/universal_ble.dart';
import '../../messages/notification.dart';
import '../bluetooth_device.dart';
class EliteSterzo extends BaseDevice {
class EliteSterzo extends BluetoothDevice {
EliteSterzo(super.scanResult)
: super(
availableButtons: [

View File

@@ -1,27 +1,15 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:gamepads/gamepads.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:universal_ble/src/models/ble_service.dart';
class Gamepad extends BaseDevice {
Gamepad(super.scanResult) : super(availableButtons: ControllerButton.values.toList(), isBeta: true);
class GamepadDevice extends BaseDevice {
final String id;
@override
Future<void> handleServices(List<BleService> services) {
// TODO: implement handleServices
throw UnimplementedError();
}
@override
Future<void> processCharacteristic(String characteristic, Uint8List bytes) {
// TODO: implement processCharacteristic
throw UnimplementedError();
}
GamepadDevice(super.name, {required this.id})
: super(availableButtons: ControllerButton.values.toList(), isBeta: true);
void processGamepadEvent(GamepadEvent event) {
print('KEy: ${event.key}');
switch (event.key) {
case 'AXIS_HAT_X':
handleButtonsClicked([ControllerButton.shiftUpLeft]);
@@ -32,4 +20,14 @@ class Gamepad extends BaseDevice {
}
handleButtonsClicked([]);
}
@override
Future<void> connect() async {}
@override
Widget showInformation(BuildContext context) {
return Row(
children: [],
);
}
}

View File

@@ -1,11 +1,12 @@
import 'dart:collection';
import 'dart:typed_data';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:universal_ble/universal_ble.dart';
class WahooKickrBikeShift extends BaseDevice {
import '../bluetooth_device.dart';
class WahooKickrBikeShift extends BluetoothDevice {
WahooKickrBikeShift(super.scanResult)
: super(
availableButtons: WahooKickrBikeShiftConstants.prefixToButton.values.toList(),

View File

@@ -3,7 +3,7 @@ import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:swift_control/bluetooth/ble.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/bluetooth/devices/bluetooth_device.dart';
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
import 'package:swift_control/bluetooth/messages/notification.dart';
import 'package:swift_control/main.dart';
@@ -11,7 +11,7 @@ import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/utils/single_line_exception.dart';
import 'package:universal_ble/universal_ble.dart';
abstract class ZwiftDevice extends BaseDevice {
abstract class ZwiftDevice extends BluetoothDevice {
ZwiftDevice(super.scanResult, {required super.availableButtons, super.isBeta});
BleCharacteristic? syncRxCharacteristic;

View File

@@ -6,14 +6,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_clickv2.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/pages/markdown.dart';
import 'package:swift_control/pages/touch_area.dart';
import 'package:swift_control/utils/actions/desktop.dart';
import 'package:swift_control/utils/actions/link.dart';
import 'package:swift_control/utils/keymap/manager.dart';
import 'package:swift_control/widgets/beta_pill.dart';
import 'package:swift_control/widgets/ingameactions_customizer.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
import 'package:swift_control/widgets/loading_widget.dart';
@@ -121,7 +119,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
final canVibrate = connection.devices.any(
final canVibrate = connection.bluetoothDevices.any(
(device) => (device.device.name == 'Zwift Ride' || device.device.name == 'Zwift Play') && device.isConnected,
);
@@ -174,37 +172,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
children: [
if (connection.devices.isEmpty) Text('No devices connected. Searching...'),
...connection.devices.map(
(device) => Row(
children: [
Text(
device.device.name?.screenshot ?? device.runtimeType.toString(),
style: TextStyle(fontWeight: FontWeight.bold),
),
if (device.isBeta) BetaPill(),
if (device.batteryLevel != null) ...[
Icon(switch (device.batteryLevel!) {
>= 80 => Icons.battery_full,
>= 60 => Icons.battery_6_bar,
>= 50 => Icons.battery_5_bar,
>= 25 => Icons.battery_4_bar,
>= 10 => Icons.battery_2_bar,
_ => Icons.battery_alert,
}),
Text('${device.batteryLevel}%'),
if (device.firmwareVersion != null) Text(' - Firmware: ${device.firmwareVersion}'),
if (device.firmwareVersion != null &&
device is ZwiftDevice &&
device.firmwareVersion != device.latestFirmwareVersion) ...[
SizedBox(width: 8),
Icon(Icons.warning, color: Theme.of(context).colorScheme.error),
Text(
' (latest: ${device.latestFirmwareVersion})',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
],
],
),
(device) => device.showInformation(context),
),
if (actionHandler is RemoteActions)
Row(

View File

@@ -39,7 +39,7 @@ class _InGameActionsCustomizerState extends State<InGameActionsCustomizer> {
Padding(
padding: const EdgeInsets.all(6),
child: Text(
'Button on your ${connectedDevice?.device.name?.screenshot ?? connectedDevice?.runtimeType ?? 'device'}',
'Button on your ${connectedDevice?.name.screenshot ?? connectedDevice?.runtimeType ?? 'device'}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
),

View File

@@ -56,7 +56,7 @@ class KeymapExplanation extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(6),
child: Text(
'Button on your ${connectedDevice?.device.name?.screenshot ?? connectedDevice?.runtimeType ?? 'device'}',
'Button on your ${connectedDevice?.name.screenshot ?? connectedDevice?.runtimeType ?? 'device'}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
),