mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
add new remote keyboard connection method
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
### 4.7.0 (04-02-2026)
|
||||
|
||||
**Features**:
|
||||
- new connection method: act as Bluetooth Keyboard:
|
||||
Your device can now act as Bluetooth keyboard, allowing you to send keyboard shortcuts (e.g. for virtual shifting) directly to your connected device. Especially useful for tablets / iPads.
|
||||
- added new keyboard shortcuts for Rouvy (Kudos, Pause workout)
|
||||
|
||||
**Fixes**:
|
||||
- save "Enable Media Key detection" setting across app restarts
|
||||
- UI adjustments and fixes in the controller configuration screen
|
||||
|
||||
418
lib/bluetooth/remote_keyboard_pairing.dart
Normal file
418
lib/bluetooth/remote_keyboard_pairing.dart
Normal file
@@ -0,0 +1,418 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/requirements/multi.dart';
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
|
||||
import '../utils/keymap/keymap.dart';
|
||||
|
||||
class RemoteKeyboardPairing extends TrainerConnection {
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
late final _peripheralManager = PeripheralManager();
|
||||
bool _isLoading = false;
|
||||
bool _isServiceAdded = false;
|
||||
bool _isSubscribedToEvents = false;
|
||||
|
||||
Central? _central;
|
||||
GATTCharacteristic? _inputReport;
|
||||
|
||||
static const String connectionTitle = 'Keyboard Remote Control';
|
||||
|
||||
RemoteKeyboardPairing()
|
||||
: super(
|
||||
title: connectionTitle,
|
||||
supportedActions: InGameAction.values,
|
||||
);
|
||||
|
||||
Future<void> reconnect() async {
|
||||
await _peripheralManager.stopAdvertising();
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
startAdvertising().catchError((e) {
|
||||
core.settings.setRemoteControlEnabled(false);
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_WARNING, 'Failed to start Remote Control pairing: $e'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> startAdvertising() async {
|
||||
_isLoading = true;
|
||||
isStarted.value = true;
|
||||
|
||||
_peripheralManager.stateChanged.forEach((state) {
|
||||
print('Peripheral manager state: ${state.state}');
|
||||
});
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
_peripheralManager.connectionStateChanged.forEach((state) {
|
||||
print('Peripheral connection state: ${state.state} of ${state.central.uuid}');
|
||||
if (state.state == ConnectionState.connected) {
|
||||
} else if (state.state == ConnectionState.disconnected) {
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, AppLocalizations.current.disconnected),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
final status = await Permission.bluetoothAdvertise.request();
|
||||
if (!status.isGranted) {
|
||||
print('Bluetooth advertise permission not granted');
|
||||
isStarted.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (_peripheralManager.state != BluetoothLowEnergyState.poweredOn && core.settings.getRemoteControlEnabled()) {
|
||||
print('Waiting for peripheral manager to be powered on...');
|
||||
if (core.settings.getLastTarget() == Target.thisDevice) {
|
||||
return;
|
||||
}
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
final inputReport = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A4D'),
|
||||
permissions: [GATTCharacteristicPermission.read],
|
||||
properties: [GATTCharacteristicProperty.notify, GATTCharacteristicProperty.read],
|
||||
descriptors: [
|
||||
GATTDescriptor.immutable(
|
||||
// Report Reference: ID=1, Type=Input(1)
|
||||
uuid: UUID.fromString('2908'),
|
||||
value: Uint8List.fromList([0x01, 0x01]),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (!_isServiceAdded) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
|
||||
final reportMapDataAbsolute = Uint8List.fromList([
|
||||
// Keyboard Report (Report ID 1)
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x06, // Usage (Keyboard)
|
||||
0xA1, 0x01, // Collection (Application)
|
||||
0x85, 0x01, // Report ID (1)
|
||||
0x05, 0x07, // Usage Page (Keyboard/Keypad)
|
||||
0x19, 0xE0, // Usage Minimum (Left Control)
|
||||
0x29, 0xE7, // Usage Maximum (Right GUI)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x08, // Report Count (8)
|
||||
0x81, 0x02, // Input (Data,Var,Abs) - Modifier byte
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x81, 0x01, // Input (Const) - Reserved byte
|
||||
0x95, 0x06, // Report Count (6)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x65, // Logical Maximum (101)
|
||||
0x05, 0x07, // Usage Page (Keyboard/Keypad)
|
||||
0x19, 0x00, // Usage Minimum (0)
|
||||
0x29, 0x65, // Usage Maximum (101)
|
||||
0x81, 0x00, // Input (Data,Array) - Key array (6 keys)
|
||||
0xC0, // End Collection
|
||||
]);
|
||||
|
||||
// 1) Build characteristics
|
||||
final hidInfo = GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A4A'),
|
||||
value: Uint8List.fromList([0x11, 0x01, 0x00, 0x02]),
|
||||
descriptors: [], // HID v1.11, country=0, flags=2
|
||||
);
|
||||
|
||||
final reportMap = GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A4B'),
|
||||
//properties: [GATTCharacteristicProperty.read],
|
||||
//permissions: [GATTCharacteristicPermission.read],
|
||||
value: reportMapDataAbsolute,
|
||||
descriptors: [
|
||||
GATTDescriptor.immutable(uuid: UUID.fromString('2908'), value: Uint8List.fromList([0x0, 0x0])),
|
||||
],
|
||||
);
|
||||
|
||||
final protocolMode = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A4E'),
|
||||
properties: [GATTCharacteristicProperty.read, GATTCharacteristicProperty.writeWithoutResponse],
|
||||
permissions: [GATTCharacteristicPermission.read, GATTCharacteristicPermission.write],
|
||||
descriptors: [],
|
||||
);
|
||||
|
||||
final hidControlPoint = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A4C'),
|
||||
properties: [GATTCharacteristicProperty.writeWithoutResponse],
|
||||
permissions: [GATTCharacteristicPermission.write],
|
||||
descriptors: [],
|
||||
);
|
||||
|
||||
// 2) HID service
|
||||
final hidService = GATTService(
|
||||
uuid: UUID.fromString(Platform.isIOS ? '1812' : '00001812-0000-1000-8000-00805F9B34FB'),
|
||||
isPrimary: true,
|
||||
characteristics: [
|
||||
hidInfo,
|
||||
reportMap,
|
||||
protocolMode,
|
||||
hidControlPoint,
|
||||
inputReport,
|
||||
],
|
||||
includedServices: [],
|
||||
);
|
||||
|
||||
if (!_isSubscribedToEvents) {
|
||||
_isSubscribedToEvents = true;
|
||||
_peripheralManager.characteristicReadRequested.forEach((char) {
|
||||
print('Read request for characteristic: ${char}');
|
||||
// You can respond to read requests here if needed
|
||||
});
|
||||
|
||||
_peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
// Check if this is the input report characteristic (2A4D)
|
||||
if (char.characteristic.uuid == inputReport.uuid) {
|
||||
if (char.state) {
|
||||
_central = char.central;
|
||||
_inputReport = char.characteristic;
|
||||
isConnected.value = true;
|
||||
print('Input report subscribed');
|
||||
} else {
|
||||
_inputReport = null;
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
print('Input report unsubscribed');
|
||||
}
|
||||
}
|
||||
print('Notify state changed for characteristic: ${char.characteristic.uuid}: ${char.state}');
|
||||
});
|
||||
}
|
||||
await _peripheralManager.addService(hidService);
|
||||
|
||||
// 3) Optional Battery service
|
||||
await _peripheralManager.addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180F'),
|
||||
isPrimary: true,
|
||||
characteristics: [
|
||||
GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A19'),
|
||||
value: Uint8List.fromList([100]),
|
||||
descriptors: [],
|
||||
),
|
||||
],
|
||||
includedServices: [],
|
||||
),
|
||||
);
|
||||
_isServiceAdded = true;
|
||||
}
|
||||
|
||||
final advertisement = Advertisement(
|
||||
name:
|
||||
'BikeControl ${Platform.isIOS
|
||||
? 'iOS'
|
||||
: Platform.isAndroid
|
||||
? 'Android'
|
||||
: ''}',
|
||||
serviceUUIDs: [UUID.fromString(Platform.isIOS ? '1812' : '00001812-0000-1000-8000-00805F9B34FB')],
|
||||
);
|
||||
print('Starting advertising with Remote service...');
|
||||
|
||||
try {
|
||||
await _peripheralManager.startAdvertising(advertisement);
|
||||
} catch (e) {
|
||||
if (e.toString().contains("Advertising has already started")) {
|
||||
print('Advertising already started, ignoring error');
|
||||
return;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
Future<void> stopAdvertising() async {
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
await _peripheralManager.stopAdvertising();
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
Future<void> notifyCharacteristic(Uint8List value) async {
|
||||
if (_inputReport != null && _central != null) {
|
||||
await _peripheralManager.notifyCharacteristic(_central!, _inputReport!, value: value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActionResult> sendAction(KeyPair keyPair, {required bool isKeyDown, required bool isKeyUp}) async {
|
||||
if (isKeyDown && isKeyUp) {
|
||||
await sendKeyPress(keyPair);
|
||||
return Success('Key ${keyPair.toString()} press sent');
|
||||
} else if (isKeyDown) {
|
||||
await sendKeyDown(keyPair);
|
||||
return Success('Key ${keyPair.toString()} down sent');
|
||||
} else if (isKeyUp) {
|
||||
await sendKeyUp();
|
||||
return Success('Key ${keyPair.toString()} up sent');
|
||||
}
|
||||
return NotHandled('Illegal combination');
|
||||
}
|
||||
|
||||
/// USB HID Keyboard scan codes for common keys
|
||||
static const Map<String, int> hidKeyCodes = {
|
||||
'a': 0x04,
|
||||
'b': 0x05,
|
||||
'c': 0x06,
|
||||
'd': 0x07,
|
||||
'e': 0x08,
|
||||
'f': 0x09,
|
||||
'g': 0x0A,
|
||||
'h': 0x0B,
|
||||
'i': 0x0C,
|
||||
'j': 0x0D,
|
||||
'k': 0x0E,
|
||||
'l': 0x0F,
|
||||
'm': 0x10,
|
||||
'n': 0x11,
|
||||
'o': 0x12,
|
||||
'p': 0x13,
|
||||
'q': 0x14,
|
||||
'r': 0x15,
|
||||
's': 0x16,
|
||||
't': 0x17,
|
||||
'u': 0x18,
|
||||
'v': 0x19,
|
||||
'w': 0x1A,
|
||||
'x': 0x1B,
|
||||
'y': 0x1C,
|
||||
'z': 0x1D,
|
||||
'1': 0x1E,
|
||||
'2': 0x1F,
|
||||
'3': 0x20,
|
||||
'4': 0x21,
|
||||
'5': 0x22,
|
||||
'6': 0x23,
|
||||
'7': 0x24,
|
||||
'8': 0x25,
|
||||
'9': 0x26,
|
||||
'0': 0x27,
|
||||
'enter': 0x28,
|
||||
'escape': 0x29,
|
||||
'backspace': 0x2A,
|
||||
'tab': 0x2B,
|
||||
'space': 0x2C,
|
||||
'minus': 0x2D,
|
||||
'equals': 0x2E,
|
||||
'leftbracket': 0x2F,
|
||||
'rightbracket': 0x30,
|
||||
'backslash': 0x31,
|
||||
'semicolon': 0x33,
|
||||
'quote': 0x34,
|
||||
'grave': 0x35,
|
||||
'comma': 0x36,
|
||||
'period': 0x37,
|
||||
'slash': 0x38,
|
||||
'capslock': 0x39,
|
||||
'f1': 0x3A,
|
||||
'f2': 0x3B,
|
||||
'f3': 0x3C,
|
||||
'f4': 0x3D,
|
||||
'f5': 0x3E,
|
||||
'f6': 0x3F,
|
||||
'f7': 0x40,
|
||||
'f8': 0x41,
|
||||
'f9': 0x42,
|
||||
'f10': 0x43,
|
||||
'f11': 0x44,
|
||||
'f12': 0x45,
|
||||
'printscreen': 0x46,
|
||||
'scrolllock': 0x47,
|
||||
'pause': 0x48,
|
||||
'insert': 0x49,
|
||||
'home': 0x4A,
|
||||
'pageup': 0x4B,
|
||||
'delete': 0x4C,
|
||||
'end': 0x4D,
|
||||
'pagedown': 0x4E,
|
||||
'right': 0x4F,
|
||||
'left': 0x50,
|
||||
'down': 0x51,
|
||||
'up': 0x52,
|
||||
};
|
||||
|
||||
/// Modifier key bit masks
|
||||
static const int modLeftCtrl = 0x01;
|
||||
static const int modLeftShift = 0x02;
|
||||
static const int modLeftAlt = 0x04;
|
||||
static const int modLeftGui = 0x08;
|
||||
static const int modRightCtrl = 0x10;
|
||||
static const int modRightShift = 0x20;
|
||||
static const int modRightAlt = 0x40;
|
||||
static const int modRightGui = 0x80;
|
||||
|
||||
/// Create a keyboard HID report
|
||||
/// [modifiers] - bit mask for modifier keys (Ctrl, Shift, Alt, GUI)
|
||||
/// [keyCodes] - list of up to 6 key codes to send
|
||||
Uint8List keyboardReport(int modifiers, List<int> keyCodes) {
|
||||
final keys = List<int>.filled(6, 0);
|
||||
for (var i = 0; i < keyCodes.length && i < 6; i++) {
|
||||
keys[i] = keyCodes[i];
|
||||
}
|
||||
// Report format: [modifiers, reserved, key1, key2, key3, key4, key5, key6]
|
||||
return Uint8List.fromList([modifiers, 0x00, ...keys]);
|
||||
}
|
||||
|
||||
/// Send a keyboard key press and release
|
||||
/// [key] - the key name (e.g., 'a', 'enter', 'space', 'f1', 'up', 'down')
|
||||
/// [modifiers] - optional modifier keys (use modLeftCtrl, modLeftShift, etc.)
|
||||
Future<void> sendKeyPress(KeyPair keyPair, {int modifiers = 0}) async {
|
||||
final usbHidUsage = keyPair.physicalKey!.usbHidUsage;
|
||||
final keyCode = usbHidUsage & 0xFF;
|
||||
|
||||
// Send key down
|
||||
final downReport = keyboardReport(modifiers, [keyCode]);
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'Sending keyboard key down: $keyPair (0x${keyCode.toRadixString(16)}) with modifiers: 0x${modifiers.toRadixString(16)}',
|
||||
);
|
||||
}
|
||||
await notifyCharacteristic(downReport);
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 20));
|
||||
|
||||
// Send key up (empty report)
|
||||
final upReport = keyboardReport(0, []);
|
||||
if (kDebugMode) {
|
||||
print('Sending keyboard key up');
|
||||
}
|
||||
await notifyCharacteristic(upReport);
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
|
||||
/// Send a key down event only (for holding keys)
|
||||
Future<void> sendKeyDown(KeyPair keyPair, {int modifiers = 0}) async {
|
||||
final usbHidUsage = keyPair.physicalKey!.usbHidUsage;
|
||||
final keyCode = usbHidUsage & 0xFF;
|
||||
|
||||
final report = keyboardReport(modifiers, [keyCode]);
|
||||
await notifyCharacteristic(report);
|
||||
}
|
||||
|
||||
/// Send a key up event (release all keys)
|
||||
Future<void> sendKeyUp() async {
|
||||
final report = keyboardReport(0, []);
|
||||
await notifyCharacteristic(report);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• Die App überwacht, welches Trainings-App-Fenster aktiv ist, um sicherzustellen, dass Gesten an die richtige App gesendet werden.",
|
||||
"accessibilityUsageNoData": "• Über diesen Dienst werden keine personenbezogenen Daten abgerufen oder erfasst.",
|
||||
"accessories": "Zubehör",
|
||||
"actAsBluetoothKeyboard": "Als Bluetooth-Tastatur fungieren",
|
||||
"action": "Aktion",
|
||||
"adjustControllerButtons": "Controller-Tasten anpassen",
|
||||
"afterDate": "Nach dem {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Batterie",
|
||||
"beforeDate": "Vor dem {date}",
|
||||
"bluetoothAdvertiseAccess": "Bluetooth-Zugriff",
|
||||
"bluetoothKeyboardExplanation": "So können Sie Ihr Smartphone als Bluetooth-Tastatur zur Steuerung kompatibler Apps verwenden. Nach der Kopplung können Sie die Tasten Ihres Fahrradcontrollers nutzen, um Tastatureingaben an die App zu senden.",
|
||||
"bluetoothTurnedOn": "Bluetooth ist eingeschaltet",
|
||||
"browserNotSupported": "Dieser Browser unterstützt kein Web-Bluetooth und die Plattform wird nicht unterstützt :(",
|
||||
"button": "Taste.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Aktivieren Sie zuerst die Methode „Lokale Verbindung“.",
|
||||
"enableMediaKeyDetection": "Medientastenerkennung aktivieren",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Aktiviere zuerst MyWhoosh Link in den Verbindungseinstellungen.",
|
||||
"enablePairingProcess": "Kopplungsprozess aktivieren",
|
||||
"enablePairingProcess": "Als Bluetooth-Maus fungieren",
|
||||
"enablePermissions": "Berechtigungen aktivieren",
|
||||
"enableSteeringWithPhone": "Lenkung über Handy-Sensoren aktivieren",
|
||||
"enableVibrationFeedback": "Vibrationsfeedback beim Gangwechsel aktivieren",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Tolle Neuigkeiten! {trainerApp} unterstützt das OpenBikeControl-Protokoll für eine bestmögliche Benutzererfahrung.",
|
||||
"openBikeControlConnection": " z. B. durch Verwendung einer OpenBikeControl-Verbindung",
|
||||
"otherConnectionMethods": "Andere Verbindungsmethoden",
|
||||
"pairingDescription": "Die Kopplung ermöglicht volle Anpassungsmöglichkeiten, funktioniert aber möglicherweise nicht auf allen Geräten.",
|
||||
"pairingDescription": "So können Sie Ihr Smartphone als Bluetooth-Maus zur Steuerung von Apps verwenden. Nach der Kopplung können Sie die Tasten Ihres Fahrradcontrollers nutzen, um Mausbewegungen an die App zu senden.",
|
||||
"pairingInstructions": "Gehe auf Deinem {targetName} in die Bluetooth-Einstellungen und suche nach BikeControl oder dem Namen Ihres Geräts. Wenn Du die Fernbedienungsfunktion nutzen möchtest, ist eine Kopplung erforderlich.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• The app monitors which training app window is active to ensure gestures are sent to the correct app",
|
||||
"accessibilityUsageNoData": "• No personal data is accessed or collected through this service",
|
||||
"accessories": "Accessories",
|
||||
"actAsBluetoothKeyboard": "Act as Bluetooth Keyboard",
|
||||
"action": "Action",
|
||||
"adjustControllerButtons": "Adjust Controller Buttons",
|
||||
"afterDate": "After {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Battery",
|
||||
"beforeDate": "Before {date}",
|
||||
"bluetoothAdvertiseAccess": "Bluetooth Advertise access",
|
||||
"bluetoothKeyboardExplanation": "This will allow you to use your phone as a Bluetooth keyboard to control compatible apps. Once paired, you can use the buttons on your bike controller to send keyboard inputs to the app.",
|
||||
"bluetoothTurnedOn": "Bluetooth turned on",
|
||||
"browserNotSupported": "This Browser does not support Web Bluetooth and platform is not supported :(",
|
||||
"button": "button.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Enable Local Connection method, first.",
|
||||
"enableMediaKeyDetection": "Enable Media Key Detection",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Enable MyWhoosh Link in the connection settings first.",
|
||||
"enablePairingProcess": "Enable Pairing Process",
|
||||
"enablePairingProcess": "Act as Bluetooth Mouse",
|
||||
"enablePermissions": "Enable Permissions",
|
||||
"enableSteeringWithPhone": "Enable Steering with your phone's sensors",
|
||||
"enableVibrationFeedback": "Enable vibration feedback when shifting gears",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Great news - {trainerApp} supports the OpenBikeControl Protocol, so you'll have the best possible experience!",
|
||||
"openBikeControlConnection": " e.g. by using OpenBikeControl connection",
|
||||
"otherConnectionMethods": "Other Connection Methods",
|
||||
"pairingDescription": "Pairing allows full customizability, but may not work on all devices.",
|
||||
"pairingDescription": "This will allow you to use your phone as a Bluetooth mouse to control apps. Once paired, you can use the buttons on your bike controller to send mouse inputs to the app.",
|
||||
"pairingInstructions": "On your {targetName} go into Bluetooth settings and look for BikeControl or your machines name. Pairing is required if you want to use the remote control feature.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• L'application surveille quelle fenêtre de l'application d'entraînement est active afin de s'assurer que les gestes sont envoyés à la bonne application.",
|
||||
"accessibilityUsageNoData": "• Aucune donnée personnelle n'est consultée ou collectée par le biais de ce service.",
|
||||
"accessories": "Accessoires",
|
||||
"actAsBluetoothKeyboard": "Fonctionne comme un clavier Bluetooth",
|
||||
"action": "Action",
|
||||
"adjustControllerButtons": "Ajuster les boutons de la manette",
|
||||
"afterDate": "Après {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Batterie",
|
||||
"beforeDate": "Avant {date}",
|
||||
"bluetoothAdvertiseAccess": "Accès à la publicité Bluetooth",
|
||||
"bluetoothKeyboardExplanation": "Vous pourrez ainsi utiliser votre téléphone comme clavier Bluetooth pour contrôler les applications compatibles. Une fois l'appairage effectué, vous pourrez utiliser les boutons de votre contrôleur de vélo pour envoyer des commandes à l'application.",
|
||||
"bluetoothTurnedOn": "Bluetooth activé",
|
||||
"browserNotSupported": "Ce navigateur ne prend pas en charge Web Bluetooth et la plateforme n'est pas prise en charge :(",
|
||||
"button": "bouton.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Activez d'abord le mode de connexion locale.",
|
||||
"enableMediaKeyDetection": "Activer la détection des touches multimédias",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Activez d'abord MyWhoosh Link dans les paramètres de connexion.",
|
||||
"enablePairingProcess": "Activer le processus d'appairage",
|
||||
"enablePairingProcess": "Fonctionne comme une souris Bluetooth",
|
||||
"enablePermissions": "Activer les autorisations",
|
||||
"enableSteeringWithPhone": "Activez la direction avec les capteurs de votre téléphone",
|
||||
"enableVibrationFeedback": "Activer le retour haptique par vibration lors du changement de vitesse",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Excellente nouvelle! {trainerApp} Il prend en charge le protocole OpenBikeControl, vous bénéficierez donc de la meilleure expérience possible !",
|
||||
"openBikeControlConnection": " par exemple en utilisant la connexion OpenBikeControl",
|
||||
"otherConnectionMethods": "Autres méthodes de connexion",
|
||||
"pairingDescription": "Le jumelage permet une personnalisation complète, mais peut ne pas fonctionner sur tous les appareils.",
|
||||
"pairingDescription": "Cela vous permettra d'utiliser votre téléphone comme une souris Bluetooth pour contrôler des applications. Une fois l'appairage effectué, vous pourrez utiliser les boutons de votre contrôleur de vélo pour envoyer des commandes de souris à l'application.",
|
||||
"pairingInstructions": "Sur votre {targetName}, accédez aux paramètres Bluetooth et recherchez BikeControl ou le nom de votre appareil. L'appairage est nécessaire si vous souhaitez utiliser la fonction de commande à distance.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• L'applicazione monitora quale finestra dell'app di allenamento è attiva per garantire che i gesti vengano inviati all'app corretta",
|
||||
"accessibilityUsageNoData": "• Nessun dato personale viene raccolto o consultato tramite questo servizio",
|
||||
"accessories": "Accessori",
|
||||
"actAsBluetoothKeyboard": "Funziona come tastiera Bluetooth",
|
||||
"action": "Azione",
|
||||
"adjustControllerButtons": "Regola i pulsanti del controller",
|
||||
"afterDate": "Dopo il {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Batteria",
|
||||
"beforeDate": "Prima del {date}",
|
||||
"bluetoothAdvertiseAccess": "Accesso pubblicitario Bluetooth",
|
||||
"bluetoothKeyboardExplanation": "Questo ti permetterà di usare il tuo telefono come tastiera Bluetooth per controllare le app compatibili. Una volta associato, potrai usare i pulsanti del controller della tua bici per inviare input dalla tastiera all'app.",
|
||||
"bluetoothTurnedOn": "Bluetooth attivato",
|
||||
"browserNotSupported": "Questo browser non supporta il Web Bluetooth e la piattaforma non è supportata :(",
|
||||
"button": "pulsante.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Abilitare prima il metodo di connessione locale.",
|
||||
"enableMediaKeyDetection": "Abilita rilevamento tasti multimediali",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Per prima cosa, abilita MyWhoosh Link nelle impostazioni di connessione.",
|
||||
"enablePairingProcess": "Abilita processo di associazione",
|
||||
"enablePairingProcess": "Funziona come un mouse Bluetooth",
|
||||
"enablePermissions": "Abilita autorizzazioni",
|
||||
"enableSteeringWithPhone": "Abilita lo sterzo con i sensori del tuo telefono",
|
||||
"enableVibrationFeedback": "Abilita il feedback delle vibrazioni durante il cambio marcia",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Ottime notizie -{trainerApp} supporta il protocollo OpenBikeControl, così avrai la migliore esperienza possibile!",
|
||||
"openBikeControlConnection": " ad esempio utilizzando la connessione OpenBikeControl",
|
||||
"otherConnectionMethods": "Altri metodi di connessione",
|
||||
"pairingDescription": "L'associazione consente la personalizzazione completa, ma potrebbe non funzionare su tutti i dispositivi.",
|
||||
"pairingDescription": "Questo ti permetterà di usare il tuo telefono come mouse Bluetooth per controllare le app. Una volta associato, potrai usare i pulsanti del controller della tua bici per inviare input del mouse all'app.",
|
||||
"pairingInstructions": "Sul tuo{targetName} Accedi alle impostazioni Bluetooth e cerca BikeControl o il nome del tuo dispositivo. L'associazione è necessaria se vuoi utilizzare la funzione di controllo remoto.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• Aplikacja monitoruje, które okno aplikacji treningowej jest aktywne, aby zapewnić, że gesty są wysyłane do właściwej aplikacji",
|
||||
"accessibilityUsageNoData": "• Ta usługa nie uzyskuje dostępu do danych osobowych, ani ich nie gromadzi",
|
||||
"accessories": "Akcesoria",
|
||||
"actAsBluetoothKeyboard": "Działa jako klawiatura Bluetooth",
|
||||
"action": "Działanie",
|
||||
"adjustControllerButtons": "Dostosuj przyciski kontrolera",
|
||||
"afterDate": "Po {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Bateria",
|
||||
"beforeDate": "Zanim {date}",
|
||||
"bluetoothAdvertiseAccess": "Dostęp do reklamy Bluetooth",
|
||||
"bluetoothKeyboardExplanation": "Umożliwi to używanie telefonu jako klawiatury Bluetooth do sterowania kompatybilnymi aplikacjami. Po sparowaniu możesz używać przycisków na kontrolerze rowerowym do wysyłania poleceń z klawiatury do aplikacji.",
|
||||
"bluetoothTurnedOn": "Włączono Bluetooth",
|
||||
"browserNotSupported": "Ta przeglądarka nie obsługuje technologii Web Bluetooth i platforma nie jest obsługiwana :(",
|
||||
"button": "przycisk.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Najpierw włącz metodę połączenia lokalnego.",
|
||||
"enableMediaKeyDetection": "Włącz rozpoznawanie klawiszy multimedialnych",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Najpierw włącz MyWhoosh Link w ustawieniach połączenia.",
|
||||
"enablePairingProcess": "Włącz proces parowania",
|
||||
"enablePairingProcess": "Działa jako mysz Bluetooth",
|
||||
"enablePermissions": "Nadaj uprawnienia",
|
||||
"enableSteeringWithPhone": "Włącz sterowanie za pomocą czujników telefonu",
|
||||
"enableVibrationFeedback": "Włącz wibracje podczas zmiany biegów",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Świetna wiadomość - {trainerApp} obsługuje protokół OpenBikeControl, dzięki czemu uzyskasz najlepsze doświadczenia!",
|
||||
"openBikeControlConnection": " np. za pomocą połączenia OpenBikeControl",
|
||||
"otherConnectionMethods": "Inne metody połączenia",
|
||||
"pairingDescription": "Parowanie umożliwia pełną personalizację, jednak może nie działać na wszystkich urządzeniach.",
|
||||
"pairingDescription": "Umożliwi to używanie telefonu jako myszy Bluetooth do sterowania aplikacjami. Po sparowaniu możesz używać przycisków na kontrolerze rowerowym do wysyłania poleceń myszy do aplikacji.",
|
||||
"pairingInstructions": "Przejdź do ustawień Bluetooth na twoim {targetName} i wyszukaj BikeControl lub nazwę swojego urządzenia. Parowanie jest wymagane, jeśli chcesz korzystać z funkcji zdalnego sterowania.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -180,14 +180,17 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
SizedBox(height: 8),
|
||||
ColoredTitle(text: 'Local / Remote Setting'),
|
||||
|
||||
if (core.actionHandler.supportedModes.contains(SupportedMode.keyboard))
|
||||
if (core.actionHandler.supportedModes.contains(SupportedMode.keyboard) &&
|
||||
(core.settings.getLocalEnabled() || core.settings.getRemoteKeyboardControlEnabled()))
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return SelectableCard(
|
||||
icon: RadixIcons.keyboard,
|
||||
title: Text(context.i18n.simulateKeyboardShortcut),
|
||||
isActive:
|
||||
_keyPair.physicalKey != null && !_keyPair.isSpecialKey && core.settings.getLocalEnabled(),
|
||||
_keyPair.physicalKey != null &&
|
||||
!_keyPair.isSpecialKey &&
|
||||
(core.settings.getLocalEnabled() || core.settings.getRemoteKeyboardControlEnabled()),
|
||||
value: _keyPair.toString(),
|
||||
onPressed: () async {
|
||||
await _showModeDropdown(context, SupportedMode.keyboard);
|
||||
@@ -195,7 +198,8 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
if (core.actionHandler.supportedModes.contains(SupportedMode.touch))
|
||||
if (core.actionHandler.supportedModes.contains(SupportedMode.touch) &&
|
||||
(core.settings.getLocalEnabled() || core.settings.getRemoteControlEnabled()))
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return SelectableCard(
|
||||
@@ -538,7 +542,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
.distinctBy((kp) => kp.inGameAction)
|
||||
.toList();
|
||||
|
||||
if (!core.settings.getLocalEnabled() && !core.settings.getRemoteControlEnabled()) {
|
||||
if (!core.settings.getLocalEnabled() && !core.settings.getRemoteKeyboardControlEnabled()) {
|
||||
return buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: AppLocalizations.of(context).enableLocalConnectionMethodFirst,
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:bike_control/bluetooth/devices/openbikecontrol/obc_mdns_emulator
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/ftms_mdns_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/remote_keyboard_pairing.dart';
|
||||
import 'package:bike_control/bluetooth/remote_pairing.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/pages/touch_area.dart';
|
||||
@@ -22,7 +23,8 @@ import 'package:bike_control/widgets/apps/openbikecontrol_ble_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/openbikecontrol_mdns_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/zwift_mdns_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/zwift_tile.dart';
|
||||
import 'package:bike_control/widgets/pair_widget.dart';
|
||||
import 'package:bike_control/widgets/keyboard_pair_widget.dart';
|
||||
import 'package:bike_control/widgets/mouse_pair_widget.dart';
|
||||
import 'package:bike_control/widgets/ui/gradient_text.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:bike_control/widgets/ui/warning.dart';
|
||||
@@ -232,12 +234,21 @@ class _ButtonSimulatorState extends State<ButtonSimulator> {
|
||||
),
|
||||
OpenBikeControlMdnsEmulator.connectionTitle => OpenBikeControlMdnsTile(),
|
||||
OpenBikeControlBluetoothEmulator.connectionTitle => OpenBikeControlBluetoothTile(),
|
||||
RemotePairing.connectionTitle => RemotePairingWidget(),
|
||||
RemotePairing.connectionTitle => RemoteMousePairingWidget(),
|
||||
RemoteKeyboardPairing.connectionTitle => RemoteKeyboardPairingWidget(),
|
||||
_ => SizedBox.shrink(),
|
||||
},
|
||||
...connectedTrainers.map(
|
||||
(connection) {
|
||||
final supportedActions = connection.supportedActions;
|
||||
final supportedActions = connection.supportedActions == InGameAction.values
|
||||
? core.settings
|
||||
.getTrainerApp()!
|
||||
.keymap
|
||||
.keyPairs
|
||||
.mapNotNull((k) => k.inGameAction)
|
||||
.distinct()
|
||||
.toList()
|
||||
: connection.supportedActions;
|
||||
|
||||
final actionGroups = {
|
||||
if (supportedActions.contains(InGameAction.shiftUp) &&
|
||||
|
||||
@@ -14,7 +14,8 @@ import 'package:bike_control/widgets/apps/openbikecontrol_mdns_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/zwift_mdns_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/zwift_tile.dart';
|
||||
import 'package:bike_control/widgets/iap_status_widget.dart';
|
||||
import 'package:bike_control/widgets/pair_widget.dart';
|
||||
import 'package:bike_control/widgets/keyboard_pair_widget.dart';
|
||||
import 'package:bike_control/widgets/mouse_pair_widget.dart';
|
||||
import 'package:bike_control/widgets/ui/colored_title.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
@@ -122,10 +123,11 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
|
||||
),
|
||||
if (core.logic.showLocalControl && !showLocalAsOther) LocalTile(),
|
||||
if (core.logic.showMyWhooshLink && !showWhooshLinkAsOther) MyWhooshLinkTile(),
|
||||
if (core.logic.showRemote) RemoteKeyboardPairingWidget(),
|
||||
];
|
||||
|
||||
final otherTiles = [
|
||||
if (core.logic.showRemote) RemotePairingWidget(),
|
||||
if (core.logic.showRemote) RemoteMousePairingWidget(),
|
||||
if (showLocalAsOther) LocalTile(),
|
||||
if (showWhooshLinkAsOther) MyWhooshLinkTile(),
|
||||
];
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
|
||||
class RemoteActions extends BaseActions {
|
||||
RemoteActions({super.supportedModes = const [SupportedMode.touch]});
|
||||
RemoteActions({super.supportedModes = const [SupportedMode.touch, SupportedMode.keyboard]});
|
||||
|
||||
@override
|
||||
Future<ActionResult> performAction(ControllerButton button, {required bool isKeyDown, required bool isKeyUp}) async {
|
||||
@@ -16,14 +16,22 @@ class RemoteActions extends BaseActions {
|
||||
return superResult;
|
||||
}
|
||||
final keyPair = supportedApp!.keymap.getKeyPair(button)!;
|
||||
if (!core.remotePairing.isConnected.value) {
|
||||
if (!core.remotePairing.isConnected.value && !core.remoteKeyboardPairing.isConnected.value) {
|
||||
return Error('Not connected to a ${core.settings.getLastTarget()?.name ?? 'remote'} device');
|
||||
}
|
||||
|
||||
if (keyPair.physicalKey != null && keyPair.touchPosition == Offset.zero) {
|
||||
return Error('Physical key actions are not supported, yet');
|
||||
} else {
|
||||
if (core.remotePairing.isConnected.value) {
|
||||
if (keyPair.touchPosition == Offset.zero) {
|
||||
return Error('Key $keyPair does not have a valid touch position');
|
||||
}
|
||||
return core.remotePairing.sendAction(keyPair, isKeyDown: isKeyDown, isKeyUp: isKeyUp);
|
||||
} else if (core.remoteKeyboardPairing.isConnected.value) {
|
||||
if (keyPair.physicalKey == null) {
|
||||
return Error('Key $keyPair does not have a valid physical key for keyboard actions');
|
||||
}
|
||||
return core.remoteKeyboardPairing.sendAction(keyPair, isKeyDown: isKeyDown, isKeyUp: isKeyUp);
|
||||
} else {
|
||||
return Error('Not connected to a ${core.settings.getLastTarget()?.name ?? 'remote'} device');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import 'package:bike_control/bluetooth/devices/openbikecontrol/obc_mdns_emulator
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/protocol_parser.dart';
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/ftms_mdns_emulator.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/bluetooth/remote_keyboard_pairing.dart';
|
||||
import 'package:bike_control/bluetooth/remote_pairing.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/utils/actions/android.dart';
|
||||
@@ -21,6 +21,7 @@ import 'package:dartx/dartx.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
import '../bluetooth/connection.dart';
|
||||
@@ -44,6 +45,7 @@ class Core {
|
||||
late final obpMdnsEmulator = OpenBikeControlMdnsEmulator();
|
||||
late final obpBluetoothEmulator = OpenBikeControlBluetoothEmulator();
|
||||
late final remotePairing = RemotePairing();
|
||||
late final remoteKeyboardPairing = RemoteKeyboardPairing();
|
||||
|
||||
late final mediaKeyHandler = MediaKeyHandler();
|
||||
late final logic = CoreLogic();
|
||||
@@ -180,6 +182,10 @@ class CoreLogic {
|
||||
return core.settings.getRemoteControlEnabled() && showRemote;
|
||||
}
|
||||
|
||||
bool get isRemoteKeyboardControlEnabled {
|
||||
return core.settings.getRemoteKeyboardControlEnabled() && showRemote;
|
||||
}
|
||||
|
||||
bool get showMyWhooshLink =>
|
||||
core.settings.getTrainerApp() is MyWhoosh &&
|
||||
core.settings.getLastTarget() != null &&
|
||||
@@ -210,7 +216,7 @@ class CoreLogic {
|
||||
core.settings.getTrainerApp()?.supportsOpenBikeProtocol.isNotEmpty == true;
|
||||
|
||||
bool get showLocalRemoteOptions =>
|
||||
core.actionHandler.supportedModes.isNotEmpty && ((showLocalControl) || (isRemoteControlEnabled));
|
||||
core.actionHandler.supportedModes.isNotEmpty && (showLocalControl || isRemoteControlEnabled || isRemoteKeyboardControlEnabled);
|
||||
|
||||
bool get hasNoConnectionMethod =>
|
||||
!screenshotMode &&
|
||||
@@ -235,6 +241,7 @@ class CoreLogic {
|
||||
if (isZwiftBleEnabled) core.zwiftEmulator,
|
||||
if (isZwiftMdnsEnabled) core.zwiftMdnsEmulator,
|
||||
if (isRemoteControlEnabled) core.remotePairing,
|
||||
if (isRemoteKeyboardControlEnabled) core.remoteKeyboardPairing,
|
||||
].filter((e) => e.isConnected.value).toList();
|
||||
|
||||
List<TrainerConnection> get enabledTrainerConnections => [
|
||||
@@ -244,6 +251,7 @@ class CoreLogic {
|
||||
if (isZwiftBleEnabled) core.zwiftEmulator,
|
||||
if (isZwiftMdnsEnabled) core.zwiftMdnsEmulator,
|
||||
if (isRemoteControlEnabled) core.remotePairing,
|
||||
if (isRemoteKeyboardControlEnabled) core.remoteKeyboardPairing,
|
||||
];
|
||||
|
||||
List<TrainerConnection> get trainerConnections => [
|
||||
@@ -253,6 +261,7 @@ class CoreLogic {
|
||||
if (showZwiftBleEmulator) core.zwiftEmulator,
|
||||
if (showZwiftMsdnEmulator) core.zwiftMdnsEmulator,
|
||||
if (showRemote) core.remotePairing,
|
||||
if (showRemote) core.remoteKeyboardPairing,
|
||||
];
|
||||
|
||||
Future<bool> isTrainerConnected() async {
|
||||
@@ -329,5 +338,15 @@ class CoreLogic {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (isRemoteKeyboardControlEnabled && !core.remoteKeyboardPairing.isStarted.value) {
|
||||
core.remoteKeyboardPairing.startAdvertising().catchError((e, s) {
|
||||
recordError(e, s, context: 'Remote Keyboard Pairing');
|
||||
core.settings.setRemoteKeyboardControlEnabled(false);
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_WARNING, 'Failed to start Remote Keyboard Control pairing: $e'),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,9 @@ class RevenueCatService {
|
||||
|
||||
/// Increment the daily command count
|
||||
Future<void> incrementCommandCount() async {
|
||||
if (isPurchasedNotifier.value) {
|
||||
return; // No need to track for purchased users
|
||||
}
|
||||
try {
|
||||
final today = DateTime.now().toIso8601String().split('T')[0];
|
||||
final lastDate = await _prefs.read(key: _lastCommandDateKey);
|
||||
|
||||
@@ -50,6 +50,18 @@ class Rouvy extends SupportedApp {
|
||||
logicalKey: LogicalKeyboardKey.keyB,
|
||||
inGameAction: InGameAction.back,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.a],
|
||||
physicalKey: PhysicalKeyboardKey.keyY,
|
||||
logicalKey: LogicalKeyboardKey.keyY,
|
||||
inGameAction: InGameAction.kudos,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.y],
|
||||
physicalKey: PhysicalKeyboardKey.keyZ,
|
||||
logicalKey: LogicalKeyboardKey.keyZ,
|
||||
inGameAction: InGameAction.pause,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -31,6 +31,10 @@ enum InGameAction {
|
||||
back('Back', icon: BootstrapIcons.arrowLeft),
|
||||
rideOnBomb('Ride On Bomb', icon: LucideIcons.bomb, isLongPress: true),
|
||||
|
||||
// rouvy
|
||||
kudos('Kudos', icon: BootstrapIcons.handThumbsUp),
|
||||
pause('Pause/Resume', icon: BootstrapIcons.pause, isLongPress: true),
|
||||
|
||||
// headwind
|
||||
headwindSpeed('Headwind Speed', possibleValues: [0, 25, 50, 75, 100]),
|
||||
headwindHeartRateMode('Headwind HR Mode'),
|
||||
@@ -95,7 +99,7 @@ class ControllerButton {
|
||||
}
|
||||
|
||||
String get displayName {
|
||||
if (sourceDeviceId == null || true) {
|
||||
if (sourceDeviceId == null) {
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
@@ -197,10 +197,9 @@ class KeyPair {
|
||||
|
||||
bool get hasActiveAction =>
|
||||
screenshotMode ||
|
||||
(physicalKey != null &&
|
||||
core.logic.showLocalControl &&
|
||||
core.settings.getLocalEnabled() &&
|
||||
core.actionHandler.supportedModes.contains(SupportedMode.keyboard)) ||
|
||||
(physicalKey != null && (core.logic.showLocalControl && core.settings.getLocalEnabled()) ||
|
||||
(core.logic.showRemote && core.settings.getRemoteKeyboardControlEnabled()) &&
|
||||
core.actionHandler.supportedModes.contains(SupportedMode.keyboard)) ||
|
||||
(isSpecialKey &&
|
||||
core.logic.showLocalControl &&
|
||||
core.settings.getLocalEnabled() &&
|
||||
|
||||
@@ -319,6 +319,14 @@ class Settings {
|
||||
return prefs.getBool('remote_control_enabled') ?? false;
|
||||
}
|
||||
|
||||
void setRemoteKeyboardControlEnabled(bool value) {
|
||||
prefs.setBool('remote_keyboard_control_enabled', value);
|
||||
}
|
||||
|
||||
bool getRemoteKeyboardControlEnabled() {
|
||||
return prefs.getBool('remote_keyboard_control_enabled') ?? false;
|
||||
}
|
||||
|
||||
bool getLocalEnabled() {
|
||||
return prefs.getBool('local_control_enabled') ?? false;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../utils/keymap/apps/custom_app.dart';
|
||||
|
||||
@@ -152,8 +154,48 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 20,
|
||||
children: [
|
||||
Text(AppLocalizations.current.pressKeyToAssign(_pressedButton.toString())),
|
||||
Text(
|
||||
AppLocalizations.current.pressKeyToAssign(_pressedButton?.displayName ?? _pressedButton.toString()),
|
||||
),
|
||||
Text(_formatKey(_pressedKey)),
|
||||
if (kDebugMode && (Platform.isAndroid || Platform.isIOS))
|
||||
SizedBox(
|
||||
height: 300,
|
||||
width: 300,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: LogicalKeyboardKey.knownLogicalKeys
|
||||
.map(
|
||||
(key) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
minVerticalPadding: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
Chip(label: Text(key.keyLabel)),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_pressedKey = KeyDownEvent(
|
||||
physicalKey: PhysicalKeyboardKey(0x80),
|
||||
logicalKey: key,
|
||||
character: null,
|
||||
timeStamp: Duration.zero,
|
||||
);
|
||||
widget.customApp.setKey(
|
||||
_pressedButton!,
|
||||
physicalKey: _pressedKey!.physicalKey,
|
||||
logicalKey: key,
|
||||
modifiers: _activeModifiers.toList(),
|
||||
touchPosition: widget.keyPair?.touchPosition,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
53
lib/widgets/keyboard_pair_widget.dart
Normal file
53
lib/widgets/keyboard_pair_widget.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/widgets/ui/connection_method.dart';
|
||||
import 'package:prop/prop.dart' show LogLevel;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
class RemoteKeyboardPairingWidget extends StatefulWidget {
|
||||
const RemoteKeyboardPairingWidget({super.key});
|
||||
|
||||
@override
|
||||
State<RemoteKeyboardPairingWidget> createState() => _PairWidgetState();
|
||||
}
|
||||
|
||||
class _PairWidgetState extends State<RemoteKeyboardPairingWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: core.remoteKeyboardPairing.isStarted,
|
||||
builder: (context, isStarted, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: core.remoteKeyboardPairing.isConnected,
|
||||
builder: (context, isConnected, child) {
|
||||
return ConnectionMethod(
|
||||
supportedActions: null,
|
||||
isEnabled: core.logic.isRemoteKeyboardControlEnabled,
|
||||
isStarted: isStarted,
|
||||
showTroubleshooting: true,
|
||||
type: ConnectionMethodType.bluetooth,
|
||||
instructionLink: 'https://youtube.com/shorts/qalBSiAz7wg',
|
||||
title: AppLocalizations.of(context).actAsBluetoothKeyboard,
|
||||
description: AppLocalizations.of(context).bluetoothKeyboardExplanation,
|
||||
isConnected: isConnected,
|
||||
requirements: core.permissions.getRemoteControlRequirements(),
|
||||
onChange: (value) async {
|
||||
core.settings.setRemoteKeyboardControlEnabled(value);
|
||||
if (!value) {
|
||||
core.remoteKeyboardPairing.stopAdvertising();
|
||||
} else {
|
||||
core.remoteKeyboardPairing.startAdvertising().catchError((e) {
|
||||
core.settings.setRemoteControlEnabled(false);
|
||||
core.connection.signalNotification(AlertNotification(LogLevel.LOGLEVEL_ERROR, e.toString()));
|
||||
});
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,14 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import '../utils/requirements/multi.dart';
|
||||
|
||||
class RemotePairingWidget extends StatefulWidget {
|
||||
const RemotePairingWidget({super.key});
|
||||
class RemoteMousePairingWidget extends StatefulWidget {
|
||||
const RemoteMousePairingWidget({super.key});
|
||||
|
||||
@override
|
||||
State<RemotePairingWidget> createState() => _PairWidgetState();
|
||||
State<RemoteMousePairingWidget> createState() => _PairWidgetState();
|
||||
}
|
||||
|
||||
class _PairWidgetState extends State<RemotePairingWidget> {
|
||||
class _PairWidgetState extends State<RemoteMousePairingWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
@@ -12,6 +12,7 @@ import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
enum ConnectionMethodType {
|
||||
bluetooth,
|
||||
@@ -163,13 +164,19 @@ class _ConnectionMethodState extends State<ConnectionMethod> with WidgetsBinding
|
||||
style: widget.isEnabled && Theme.of(context).brightness == Brightness.light
|
||||
? ButtonStyle.outline().withBorder(border: Border.all(color: Colors.gray.shade500))
|
||||
: ButtonStyle.outline(),
|
||||
leading: Icon(Icons.help_outline),
|
||||
leading: Icon(
|
||||
widget.instructionLink!.contains("youtube") ? Icons.ondemand_video : Icons.help_outline,
|
||||
),
|
||||
onPressed: () {
|
||||
openDrawer(
|
||||
context: context,
|
||||
position: OverlayPosition.bottom,
|
||||
builder: (c) => MarkdownPage(assetPath: widget.instructionLink!),
|
||||
);
|
||||
if (widget.instructionLink!.contains("youtube")) {
|
||||
launchUrlString(widget.instructionLink!);
|
||||
} else {
|
||||
openDrawer(
|
||||
context: context,
|
||||
position: OverlayPosition.bottom,
|
||||
builder: (c) => MarkdownPage(assetPath: widget.instructionLink!),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).instructions),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user