Compare commits

..

7 Commits

Author SHA1 Message Date
Jonas Bark
fb1a1f35ad you can now assign Escape and arrow down key to your custom keymap (fixes #18) 2025-05-04 10:46:00 +02:00
Jonas Bark
71aadde901 more troubleshooting, always use light theme 2025-05-02 19:10:56 +02:00
Jonas Bark
f7bfd8c206 UX improvements 2025-04-25 09:23:58 +02:00
Jonas Bark
ff83e5271b add Biketerra keymap (fixes #17) 2025-04-23 08:29:34 +02:00
Jonas Bark
ec6edb2864 add Biketerra keymap (fixes #17) 2025-04-18 09:44:19 +02:00
Jonas Bark
4f4a6f60c5 fix MyWhoosh up / downshift button assignment (I key vs K key) 2025-04-15 11:18:42 +02:00
Jonas Bark
354e13678b fix Zwift Click button assignment #12 2025-04-13 20:47:42 +02:00
11 changed files with 155 additions and 66 deletions

View File

@@ -1,18 +1,35 @@
#### 2.0.4 (2025-04-10)
### 2.0.9 (2025-05-04)
- you can now assign Escape and arrow down key to your custom keymap (#18)
### 2.0.8 (2025-05-02)
- only use the light theme for the app
- more troubleshooting information
### 2.0.7 (2025-04-18)
- add Biketerra.com keymap
- some UX improvements
### 2.0.6 (2025-04-15)
- fix MyWhoosh up / downshift button assignment (I key vs K key)
### 2.0.5 (2025-04-13)
- fix Zwift Click button assignment (#12)
### 2.0.4 (2025-04-10)
- vibrate Zwift Play / Zwift Ride controller on gear shift (thanks @cagnulein, closes #16)
#### 2.0.3 (2025-04-08)
### 2.0.3 (2025-04-08)
- adjust TrainingPeaks Virtual key mapping (#12)
- attempt to reconnect device if connection is lost
- Android: detect freeform windows for MyWhoosh + TrainingPeaks Virtual keymaps
#### 2.0.2 (2025-04-07)
### 2.0.2 (2025-04-07)
- fix bluetooth scan issues on older Android devices by asking for location permission
#### 2.0.1 (2025-04-06)
### 2.0.1 (2025-04-06)
- long pressing a button will trigger the action again every 250ms
#### 2.0.0 (2025-04-06)
### 2.0.0 (2025-04-06)
- You can now customize the actions (touches, mouse clicks or keyboard keys) for all buttons on all supported Zwift devices
- now shows the battery level of the connected devices
- add more troubleshooting information

View File

@@ -23,6 +23,7 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
## Supported Apps
- MyWhoosh
- indieVelo / Training Peaks
- Biketerra.com
- any other:
- Android: you can customize simulated touch points of all your buttons in the app
- Desktop: you can customize keyboard shortcuts and mouse clicks in the app
@@ -41,7 +42,8 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
- [Web](https://jonasbark.github.io/swiftcontrol/) (you won't be able to do much)
## Troubleshooting
Your Zwift device is found but connection does not work properly? You may need to update the firmware in Zwift Companion app.
- Your Zwift device is found but connection does not work properly? You may need to update the firmware in Zwift Companion app.
- The Android app is losing connection over time? Read about how to [keep the app alive](https://dontkillmyapp.com/).
## How does it work?
The app connects to your Zwift device automatically.

View File

@@ -209,7 +209,11 @@ abstract class BaseDevice {
final payload = bytes.sublist(4);
if (zapEncryption.encryptionKeyBytes == null) {
actionStreamInternal.add(LogNotification('Encryption not initialized, yet.'));
actionStreamInternal.add(
LogNotification(
'Encryption not initialized, yet. You may need to update the firmware of your device with the Zwift Companion app.',
),
);
return;
}

View File

@@ -12,8 +12,8 @@ class ClickNotification extends BaseNotification {
ClickNotification(Uint8List message) {
final status = ClickKeyPadStatus.fromBuffer(message);
buttonsClicked = [
if (status.buttonPlus == PlayButtonStatus.ON) ZwiftButton.shiftUpLeft,
if (status.buttonMinus == PlayButtonStatus.ON) ZwiftButton.shiftDownRight,
if (status.buttonPlus == PlayButtonStatus.ON) ZwiftButton.shiftUpRight,
if (status.buttonMinus == PlayButtonStatus.ON) ZwiftButton.shiftDownLeft,
];
}

View File

@@ -45,7 +45,7 @@ class SwiftPlayApp extends StatelessWidget {
title: 'SwiftControl',
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
themeMode: ThemeMode.light,
home: const RequirementsPage(),
);
}

View File

@@ -85,7 +85,9 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
final KeyPair keyPair;
actionHandler.supportedApp!.keymap.keyPairs.add(
keyPair = KeyPair(
touchPosition: context.size!.center(Offset.zero),
touchPosition: context.size!
.center(Offset.zero)
.translate(actionHandler.supportedApp!.keymap.keyPairs.length * 40, 0),
buttons: [_pressedButton!],
physicalKey: null,
logicalKey: null,
@@ -123,6 +125,7 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
onTap: () async {
await showDialog<void>(
context: context,
barrierDismissible: false, // enable Escape key
builder:
(c) =>
HotKeyListenerDialog(customApp: actionHandler.supportedApp! as CustomApp, keyPair: keyPair),
@@ -130,6 +133,49 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
setState(() {});
},
),
PopupMenuItem(
child: PopupMenuButton<PhysicalKeyboardKey>(
padding: EdgeInsets.zero,
itemBuilder:
(context) => [
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaPlayPause,
child: const Text('Media: Play/Pause'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaStop,
child: const Text('Media: Stop'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaTrackPrevious,
child: const Text('Media: Previous'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaTrackNext,
child: const Text('Media: Next'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.audioVolumeUp,
child: const Text('Media: Volume Up'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.audioVolumeDown,
child: const Text('Media: Volume Down'),
),
],
onSelected: (key) {
keyPair.physicalKey = key;
keyPair.logicalKey = null;
setState(() {});
},
child: SizedBox(
height: 50,
width: 180,
child: Align(alignment: Alignment.centerLeft, child: Text('Set Media key')),
),
),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: null,
child: const Text('Use as touch button'),
@@ -282,17 +328,25 @@ class _TouchDot extends StatelessWidget {
),
),
Text(label, style: TextStyle(color: Colors.black, fontSize: 12)),
if (keyPair.physicalKey != null)
Text(switch (keyPair.physicalKey) {
PhysicalKeyboardKey.mediaPlayPause => 'Media: Play/Pause',
PhysicalKeyboardKey.mediaStop => 'Media: Stop',
PhysicalKeyboardKey.mediaTrackPrevious => 'Media: Previous',
PhysicalKeyboardKey.mediaTrackNext => 'Media: Next',
PhysicalKeyboardKey.audioVolumeUp => 'Media: Volume Up',
PhysicalKeyboardKey.audioVolumeDown => 'Media: Volume Down',
_ => keyPair.logicalKey?.keyLabel ?? 'Unknown',
}, style: TextStyle(color: Colors.grey, fontSize: 12)),
Container(
color: Colors.white.withAlpha(180),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: Colors.black, fontSize: 12)),
if (keyPair.physicalKey != null)
Text(switch (keyPair.physicalKey) {
PhysicalKeyboardKey.mediaPlayPause => 'Media: Play/Pause',
PhysicalKeyboardKey.mediaStop => 'Media: Stop',
PhysicalKeyboardKey.mediaTrackPrevious => 'Media: Previous',
PhysicalKeyboardKey.mediaTrackNext => 'Media: Next',
PhysicalKeyboardKey.audioVolumeUp => 'Media: Volume Up',
PhysicalKeyboardKey.audioVolumeDown => 'Media: Volume Down',
_ => keyPair.logicalKey?.keyLabel ?? 'Unknown',
}, style: TextStyle(color: Colors.black87, fontSize: 12)),
],
),
),
],
);
}

View File

@@ -0,0 +1,49 @@
import 'package:accessibility/accessibility.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
import '../buttons.dart';
import '../keymap.dart';
class Biketerra extends SupportedApp {
Biketerra()
: super(
name: 'Biketerra',
packageName: "biketerra",
keymap: Keymap(
keyPairs: [
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
physicalKey: PhysicalKeyboardKey.keyS,
logicalKey: LogicalKeyboardKey.keyS,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
physicalKey: PhysicalKeyboardKey.keyW,
logicalKey: LogicalKeyboardKey.keyW,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(),
physicalKey: PhysicalKeyboardKey.arrowRight,
logicalKey: LogicalKeyboardKey.arrowRight,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(),
physicalKey: PhysicalKeyboardKey.arrowLeft,
logicalKey: LogicalKeyboardKey.arrowLeft,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(),
physicalKey: PhysicalKeyboardKey.keyU,
logicalKey: LogicalKeyboardKey.keyU,
),
],
),
);
}
extension WindowSize on WindowEvent {
int get width => right - left;
int get height => bottom - top;
}

View File

@@ -17,13 +17,13 @@ class MyWhoosh extends SupportedApp {
keyPairs: [
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
physicalKey: PhysicalKeyboardKey.keyK,
logicalKey: LogicalKeyboardKey.keyK,
physicalKey: PhysicalKeyboardKey.keyI,
logicalKey: LogicalKeyboardKey.keyI,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
physicalKey: PhysicalKeyboardKey.keyI,
logicalKey: LogicalKeyboardKey.keyI,
physicalKey: PhysicalKeyboardKey.keyK,
logicalKey: LogicalKeyboardKey.keyK,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(),

View File

@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:accessibility/accessibility.dart';
import 'package:swift_control/utils/keymap/apps/biketerra.dart';
import 'package:swift_control/utils/keymap/apps/training_peaks.dart';
import '../../single_line_exception.dart';
@@ -27,7 +28,7 @@ abstract class SupportedApp {
const SupportedApp({required this.name, required this.packageName, required this.keymap});
static final List<SupportedApp> supportedApps = [MyWhoosh(), TrainingPeaks(), CustomApp()];
static final List<SupportedApp> supportedApps = [MyWhoosh(), TrainingPeaks(), Biketerra(), CustomApp()];
@override
String toString() {

View File

@@ -67,7 +67,6 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
setState(() {
if (event is KeyDownEvent) {
_pressedKey = event;
} else if (event is KeyUpEvent) {
widget.customApp.setKey(
_pressedButton!,
physicalKey: _pressedKey!.physicalKey,
@@ -99,43 +98,6 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
children: [
Text("Press a key on your keyboard to assign to ${_pressedButton.toString()}"),
Text(_formatKey(_pressedKey)),
PopupMenuButton<PhysicalKeyboardKey>(
tooltip: 'Drag or click for special keys',
itemBuilder:
(context) => [
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaPlayPause,
child: const Text('Media: Play/Pause'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaStop,
child: const Text('Media: Stop'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaTrackPrevious,
child: const Text('Media: Previous'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaTrackNext,
child: const Text('Media: Next'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.audioVolumeUp,
child: const Text('Media: Volume Up'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.audioVolumeDown,
child: const Text('Media: Volume Down'),
),
],
onSelected: (key) {
widget.customApp.setKey(_pressedButton!, physicalKey: key, logicalKey: null);
Navigator.pop(context, key);
},
child: IgnorePointer(
child: ElevatedButton(onPressed: () {}, child: Text('Or choose special key')),
),
),
],
),
),

View File

@@ -1,7 +1,7 @@
name: swift_control
description: "SwiftControl - Control your virtual riding"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 2.0.4+0
version: 2.0.9+0
environment:
sdk: ^3.7.0