Skip to content

Commit 6c23282

Browse files
authored
Merge pull request #328 from OpenBikeControl/release
Release 5.2.0
2 parents e88a25a + 4fe5702 commit 6c23282

12 files changed

Lines changed: 143 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
### 5.2.0 (06-04-2026)
2+
Great news for MyWhoosh users: MyWhoosh has partnered with BikeControl to provide official support for controller hardware.
3+
4+
- A new network-based connection method is now available on all platforms, replacing the previous “Link” connection method
5+
- To use it, open the connection screen in MyWhoosh and tap the OpenBikeControl icon in the top right
6+
7+
This integration enables seamless and reliable controller support directly within MyWhoosh.
8+
9+
Read the full announcement for more details:
10+
https://bikecontrol.app/blog/mywhoosh-bikecontrol-partnership
11+
112
### 5.1.0 (25-03-2026)
213
**Features**:
314
- show latest blog posts from bikecontrol.app

WINDOWS_STORE_VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.1.1
1+
5.2.0

lib/bluetooth/devices/base_device.dart

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ abstract class BaseDevice {
5252

5353
static const Duration _longPressTriggerDelay = Duration(milliseconds: 550);
5454
static const Duration _doubleClickDelay = Duration(milliseconds: 320);
55+
static const Duration _repeatInterval = Duration(milliseconds: 150);
5556

5657
Timer? _longPressTimer;
5758
Timer? _singleClickTimer;
59+
Timer? _repeatTimer;
5860
Set<ControllerButton> _previouslyPressedButtons = <ControllerButton>{};
5961
Set<ControllerButton> _activeLongPressButtons = <ControllerButton>{};
6062
ControllerButton? _pendingSingleClickButton;
@@ -149,6 +151,7 @@ abstract class BaseDevice {
149151
actionStreamInternal.add(LogNotification('Buttons released'));
150152

151153
_longPressTimer?.cancel();
154+
_stopRepeatingSingleClick();
152155
final releasedButtons = _previouslyPressedButtons.toList();
153156
_previouslyPressedButtons.clear();
154157

@@ -264,7 +267,35 @@ abstract class BaseDevice {
264267
if (keyPair == null && core.actionHandler.supportedApp == null) {
265268
return trigger == ButtonTrigger.singleClick;
266269
}
267-
return keyPair != null && !keyPair.hasNoAction;
270+
if (keyPair != null && !keyPair.hasNoAction) {
271+
return true;
272+
}
273+
// Implicit repeat: long press repeats single click for Pro users by default.
274+
if (trigger == ButtonTrigger.longPress) {
275+
return _shouldRepeatSingleClick(button);
276+
}
277+
return false;
278+
}
279+
280+
/// Whether a long press should implicitly repeat the single click action.
281+
/// Active by default for Pro users when no explicit long press action is set.
282+
bool _shouldRepeatSingleClick(ControllerButton button) {
283+
if (!supportsLongPress) return false;
284+
try {
285+
if (!IAPManager.instance.isProEnabledForCurrentDevice) return false;
286+
} catch (_) {
287+
return false;
288+
}
289+
final longPressPair = core.actionHandler.supportedApp?.keymap.getKeyPair(
290+
button,
291+
trigger: ButtonTrigger.longPress,
292+
);
293+
if (longPressPair != null && !longPressPair.hasNoAction) return false;
294+
final singleClickPair = core.actionHandler.supportedApp?.keymap.getKeyPair(
295+
button,
296+
trigger: ButtonTrigger.singleClick,
297+
);
298+
return singleClickPair != null && !singleClickPair.hasNoAction;
268299
}
269300

270301
void _cancelPendingClickTimers() {
@@ -309,6 +340,12 @@ abstract class BaseDevice {
309340
continue;
310341
}
311342

343+
// Check for implicit repeat single click mode
344+
if (_shouldRepeatSingleClick(action)) {
345+
_startRepeatingSingleClick(action);
346+
continue;
347+
}
348+
312349
// For repeated actions, don't trigger key down/up events (useful for long press)
313350
final result = await core.actionHandler.performAction(
314351
action,
@@ -323,6 +360,20 @@ abstract class BaseDevice {
323360
}
324361
}
325362

363+
void _startRepeatingSingleClick(ControllerButton button) {
364+
_repeatTimer?.cancel();
365+
// Fire immediately, then repeat periodically
366+
unawaited(performClick([button], trigger: ButtonTrigger.singleClick));
367+
_repeatTimer = Timer.periodic(_repeatInterval, (_) {
368+
unawaited(performClick([button], trigger: ButtonTrigger.singleClick));
369+
});
370+
}
371+
372+
void _stopRepeatingSingleClick() {
373+
_repeatTimer?.cancel();
374+
_repeatTimer = null;
375+
}
376+
326377
Future<void> performClick(
327378
List<ControllerButton> buttonsClicked, {
328379
ButtonTrigger trigger = ButtonTrigger.singleClick,
@@ -350,7 +401,13 @@ abstract class BaseDevice {
350401
List<ControllerButton> buttonsReleased, {
351402
ButtonTrigger trigger = ButtonTrigger.longPress,
352403
}) async {
404+
_stopRepeatingSingleClick();
353405
for (final action in buttonsReleased) {
406+
// Skip normal release handling for repeat-on-long-press buttons
407+
if (_shouldRepeatSingleClick(action)) {
408+
continue;
409+
}
410+
354411
// Check IAP status before executing command
355412
if (!_canExecuteCommand()) {
356413
_showCommandLimitAlert();
@@ -371,6 +428,8 @@ abstract class BaseDevice {
371428
_longPressTimer?.cancel();
372429
_singleClickTimer?.cancel();
373430
_singleClickTimer = null;
431+
_repeatTimer?.cancel();
432+
_repeatTimer = null;
374433
_pendingSingleClickButton = null;
375434
// Release any held keys in long press mode
376435
if (core.actionHandler is DesktopActions) {

lib/pages/button_edit.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,40 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
164164
],
165165
),
166166
),
167+
if (widget.trigger == ButtonTrigger.longPress) ...[
168+
Builder(
169+
builder: (context) {
170+
final singleClickPair = widget.keymap.getKeyPair(
171+
_keyPair.buttons.first,
172+
trigger: ButtonTrigger.singleClick,
173+
);
174+
final singleClickLabel = singleClickPair != null && !singleClickPair.hasNoAction
175+
? singleClickPair.toString()
176+
: null;
177+
return SelectableCard(
178+
icon: Icons.repeat,
179+
title: Text('Repeat single click action'),
180+
isActive: _keyPair.hasNoAction,
181+
value: _keyPair.hasNoAction ? singleClickLabel : null,
182+
onPressed: () {
183+
if (!_keyPair.hasNoAction) {
184+
_keyPair.physicalKey = null;
185+
_keyPair.logicalKey = null;
186+
_keyPair.modifiers = [];
187+
_keyPair.touchPosition = Offset.zero;
188+
_keyPair.inGameAction = null;
189+
_keyPair.inGameActionValue = null;
190+
_keyPair.androidAction = null;
191+
_keyPair.command = null;
192+
_keyPair.screenshotPath = null;
193+
setState(() {});
194+
widget.onUpdate();
195+
}
196+
},
197+
);
198+
},
199+
),
200+
],
167201
if (core.logic.showObpActions) ...[
168202
ColoredTitle(text: context.i18n.openBikeControlActions),
169203
if (core.logic.obpConnectedApp == null)

lib/pages/device.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class _DevicePageState extends State<DevicePage> {
8484
children: [
8585
Row(
8686
spacing: 12,
87+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
8788
children: [
8889
Flexible(
8990
child: device.showInformation(context, showFull: false),

lib/pages/overview.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:io';
23
import 'dart:math';
34

45
import 'package:bike_control/bluetooth/devices/base_device.dart';
@@ -500,6 +501,30 @@ class _OverviewPageState extends State<OverviewPage> with TickerProviderStateMix
500501
},
501502
child: Text(context.i18n.manageIgnoredDevices).small,
502503
),
504+
if (devices.isNotEmpty)
505+
Builder(
506+
builder: (context) => IconButton.ghost(
507+
icon: Icon(Icons.more_vert, size: 16),
508+
onPressed: () {
509+
showDropdown(
510+
context: context,
511+
builder: (c) => DropdownMenu(
512+
children: [
513+
MenuButton(
514+
leading: const Icon(Icons.power_settings_new_rounded),
515+
onPressed: (c) {
516+
core.connection.disconnectAll();
517+
core.connection.stop();
518+
exit(0);
519+
},
520+
child: Text('Close BikeControl'),
521+
),
522+
],
523+
),
524+
);
525+
},
526+
),
527+
),
503528
],
504529
),
505530
),

lib/pages/trainer.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ class _TrainerPageState extends State<TrainerPage> {
8888
];
8989

9090
final otherTiles = [
91+
if (showWhooshLinkAsOther) MyWhooshLinkTile(),
9192
if (core.logic.showRemote) RemoteMousePairingWidget(),
9293
if (core.logic.showLocalControl && showLocalAsOther) LocalTile(),
93-
if (showWhooshLinkAsOther) MyWhooshLinkTile(),
9494
];
9595

9696
return Scrollbar(

lib/utils/core.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,31 +247,31 @@ class CoreLogic {
247247
showMyWhooshLink;
248248

249249
List<TrainerConnection> get connectedTrainerConnections => [
250-
if (core.settings.getLocalEnabled()) core.local,
251-
if (isMyWhooshLinkEnabled) core.whooshLink,
252250
if (isObpMdnsEnabled) core.obpMdnsEmulator,
253251
if (isObpBleEnabled) core.obpBluetoothEmulator,
252+
if (core.settings.getLocalEnabled()) core.local,
253+
if (isMyWhooshLinkEnabled) core.whooshLink,
254254
if (isZwiftBleEnabled) core.zwiftEmulator,
255255
if (isZwiftMdnsEnabled) core.zwiftMdnsEmulator,
256256
if (isRemoteControlEnabled) core.remotePairing,
257257
if (isRemoteKeyboardControlEnabled) core.remoteKeyboardPairing,
258258
].filter((e) => e.isConnected.value).toList();
259259

260260
List<TrainerConnection> get enabledTrainerConnections => [
261+
if (isObpBleEnabled) core.obpBluetoothEmulator,
262+
if (isObpMdnsEnabled) core.obpMdnsEmulator,
261263
if (core.settings.getLocalEnabled() && showLocalControl) core.local,
262264
if (isMyWhooshLinkEnabled) core.whooshLink,
263-
if (isObpMdnsEnabled) core.obpMdnsEmulator,
264-
if (isObpBleEnabled) core.obpBluetoothEmulator,
265265
if (isZwiftBleEnabled) core.zwiftEmulator,
266266
if (isZwiftMdnsEnabled) core.zwiftMdnsEmulator,
267267
if (isRemoteControlEnabled) core.remotePairing,
268268
if (isRemoteKeyboardControlEnabled) core.remoteKeyboardPairing,
269269
];
270270

271271
List<TrainerConnection> get trainerConnections => [
272-
if (showMyWhooshLink) core.whooshLink,
273272
if (showObpMdnsEmulator) core.obpMdnsEmulator,
274273
if (showObpBluetoothEmulator) core.obpBluetoothEmulator,
274+
if (showMyWhooshLink) core.whooshLink,
275275
if (showZwiftBleEmulator) core.zwiftEmulator,
276276
if (showZwiftMsdnEmulator) core.zwiftMdnsEmulator,
277277
if (showRemote) core.remotePairing,

lib/utils/keymap/apps/my_whoosh.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ class MyWhoosh extends SupportedApp {
1010
@override
1111
List<(AppConnectionMethod, ConnectionSupport)> get connections => [
1212
(AppConnectionMethod.myWhooshLink, ConnectionSupport.supported),
13+
(AppConnectionMethod.obpMdns, ConnectionSupport.supported),
1314
];
1415

1516
MyWhoosh()
1617
: super(
1718
name: 'MyWhoosh',
1819
packageName: "MyWhoosh",
1920
compatibleTargets: Target.values,
20-
star: true,
21+
star: false,
2122
additionalKeyPairs: [
2223
KeyPair(
2324
buttons: [ControllerButton('Peace', action: InGameAction.emote)],

lib/utils/requirements/platform.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ abstract class PlatformRequirement {
44
String name;
55
String? description;
66
final IconData icon;
7-
late bool status;
7+
bool status = false;
88

99
PlatformRequirement(this.name, {this.description, required this.icon});
1010

0 commit comments

Comments
 (0)