Compare commits

..

4 Commits

Author SHA1 Message Date
Jonas Bark
dc63f693f0 fix double clicks in some apps 2025-10-02 18:10:12 +02:00
Jonas Bark
455db754d8 fix #82 2025-10-01 20:33:56 +02:00
Jonas Bark
cbef8fc044 fix #82 2025-10-01 20:23:02 +02:00
Jonas Bark
d8e45f849a ui fixes, increase repeating timer 2025-10-01 20:11:01 +02:00
8 changed files with 95 additions and 102 deletions

View File

@@ -1,6 +1,7 @@
### 2.6.1 (2025-10-01)
### 2.6.3 (2025-10-01)
- fix a few issues with the new touch placement feature
- add a workaround for Zwift Click V2 which resets the device when button events are no longer sent
- fix issue on Android and Desktop where only a "touch down" was sent, but no "touch up"
### 2.6.0 (2025-09-30)
- refactor touch placements: show touches on screen, fix misplaced coordinates - should fix #64

View File

@@ -272,6 +272,9 @@ abstract class BaseDevice {
Future<List<ZwiftButton>?> processClickNotification(Uint8List message);
Future<void> handleButtonsClicked(List<ZwiftButton>? buttonsClicked) async {
final isLongPress =
buttonsClicked?.singleOrNull != null &&
actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked!.single)?.isLongPress == true;
if (buttonsClicked == null) {
// ignore, no changes
} else if (buttonsClicked.isEmpty) {
@@ -280,33 +283,28 @@ abstract class BaseDevice {
// Handle release events for long press keys
final buttonsReleased = _previouslyPressedButtons.toList();
if (buttonsReleased.isNotEmpty) {
if (buttonsReleased.isNotEmpty && isLongPress) {
await _performRelease(buttonsReleased);
}
_previouslyPressedButtons.clear();
} else {
// Handle release events for buttons that are no longer pressed
final buttonsReleased = _previouslyPressedButtons.difference(buttonsClicked.toSet()).toList();
if (buttonsReleased.isNotEmpty) {
if (buttonsReleased.isNotEmpty && isLongPress) {
await _performRelease(buttonsReleased);
}
final isLongPress =
buttonsClicked.singleOrNull != null &&
actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true;
if (!isLongPress &&
!(buttonsClicked.singleOrNull == ZwiftButton.onOffLeft ||
buttonsClicked.singleOrNull == ZwiftButton.onOffRight)) {
// we don't want to trigger the long press timer for the on/off buttons, also not when it's a long press key
_longPressTimer?.cancel();
_longPressTimer = Timer.periodic(const Duration(milliseconds: 250), (timer) async {
_longPressTimer = Timer.periodic(const Duration(milliseconds: 350), (timer) async {
_performActions(buttonsClicked, true);
});
} else if (isLongPress) {
// Update currently pressed buttons
_previouslyPressedButtons = buttonsClicked.toSet();
}
// Update currently pressed buttons
_previouslyPressedButtons = buttonsClicked.toSet();
return _performActions(buttonsClicked, false);
}
@@ -349,11 +347,11 @@ abstract class BaseDevice {
Future<void> disconnect() async {
_isInited = false;
_longPressTimer?.cancel();
_previouslyPressedButtons.clear();
// Release any held keys in long press mode
if (actionHandler is DesktopActions) {
await (actionHandler as DesktopActions).releaseAllHeldKeys();
await (actionHandler as DesktopActions).releaseAllHeldKeys(_previouslyPressedButtons.toList());
}
_previouslyPressedButtons.clear();
await UniversalBle.disconnect(device.deviceId);
isConnected = false;
}

View File

@@ -376,7 +376,7 @@ class KeypairExplanation extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
return Wrap(
spacing: 4,
children: [
if (withKey) KeyWidget(label: keyPair.buttons.joinToString(transform: (e) => e.name, separator: '\n')),

View File

@@ -44,7 +44,11 @@ class AndroidActions extends BaseActions {
final point = supportedApp!.resolveTouchPosition(action: button, windowInfo: windowInfo);
if (point != Offset.zero) {
accessibilityHandler.performTouch(point.dx, point.dy, isKeyDown: isKeyDown, isKeyUp: isKeyUp);
return "Touch performed at: ${point.dx.toInt()}, ${point.dy.toInt()}";
return "Touch performed at: ${point.dx.toInt()}, ${point.dy.toInt()} -> ${isKeyDown
? "down"
: isKeyUp
? "up"
: "click"}";
}
return "No touch performed";
}

View File

@@ -5,7 +5,6 @@ import 'package:swift_control/widgets/keymap_explanation.dart';
class DesktopActions extends BaseActions {
// Track keys that are currently held down in long press mode
final Set<ZwiftButton> _heldKeys = <ZwiftButton>{};
@override
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) async {
@@ -18,60 +17,42 @@ class DesktopActions extends BaseActions {
return ('Keymap entry not found for action: ${action.toString().splitByUpperCase()}');
}
// Handle long press mode
if (keyPair.isLongPress) {
if (isKeyDown && !isKeyUp) {
// Key press: start long press
if (!_heldKeys.contains(action)) {
_heldKeys.add(action);
if (keyPair.physicalKey != null) {
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
return 'Long press started: ${keyPair.logicalKey?.keyLabel}';
} else {
final point = supportedApp!.resolveTouchPosition(action: action, windowInfo: null);
await keyPressSimulator.simulateMouseClickDown(point);
return 'Long Mouse click started at: $point';
}
}
} else if (isKeyUp && !isKeyDown) {
// Key release: end long press
if (_heldKeys.contains(action)) {
_heldKeys.remove(action);
if (keyPair.physicalKey != null) {
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
return 'Long press ended: ${keyPair.logicalKey?.keyLabel}';
} else {
final point = supportedApp!.resolveTouchPosition(action: action, windowInfo: null);
await keyPressSimulator.simulateMouseClickUp(point);
return 'Long Mouse click ended at: $point';
}
}
}
// Ignore other combinations in long press mode
return 'Long press active';
} else {
// Handle regular key press mode (existing behavior)
if (keyPair.physicalKey != null) {
// Handle regular key press mode (existing behavior)
if (keyPair.physicalKey != null) {
if (isKeyDown) {
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
return 'Key pressed: $keyPair';
} else if (isKeyUp) {
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
return 'Key released: $keyPair';
} else {
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
return 'Key pressed: $keyPair';
return 'Key clicked: $keyPair';
}
} else {
final point = supportedApp!.resolveTouchPosition(action: action, windowInfo: null);
if (isKeyDown) {
await keyPressSimulator.simulateMouseClickDown(point);
return 'Mouse down at: ${point.dx} ${point.dy}';
} else if (isKeyUp) {
await keyPressSimulator.simulateMouseClickUp(point);
return 'Mouse up at: ${point.dx} ${point.dy}';
} else {
final point = supportedApp!.resolveTouchPosition(action: action, windowInfo: null);
await keyPressSimulator.simulateMouseClickDown(point);
await keyPressSimulator.simulateMouseClickUp(point);
return 'Mouse clicked at: ${point.dx} ${point.dy}';
}
return 'Mouse clicked at: ${point.dx} ${point.dy}';
}
}
// Release all held keys (useful for cleanup)
Future<void> releaseAllHeldKeys() async {
for (final action in _heldKeys.toList()) {
Future<void> releaseAllHeldKeys(List<ZwiftButton> list) async {
for (final action in list) {
final keyPair = supportedApp?.keymap.getKeyPair(action);
if (keyPair?.physicalKey != null) {
await keyPressSimulator.simulateKeyUp(keyPair!.physicalKey);
}
}
_heldKeys.clear();
}
}

View File

@@ -106,21 +106,23 @@ class KeyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
constraints: BoxConstraints(minWidth: 30),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.primary),
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.primaryContainer,
),
child: Center(
child: Text(
label.splitByUpperCase(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Theme.of(context).colorScheme.onPrimaryContainer,
return IntrinsicWidth(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
constraints: BoxConstraints(minWidth: 30),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.primary),
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.primaryContainer,
),
child: Center(
child: Text(
label.splitByUpperCase(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
),

View File

@@ -49,39 +49,46 @@ class _LogviewerState extends State<LogViewer> {
@override
Widget build(BuildContext context) {
return SelectionArea(
child: ListView(
physics: const NeverScrollableScrollPhysics(),
controller: _scrollController,
shrinkWrap: true,
reverse: true,
children:
_actions
.map(
(action) => Text.rich(
TextSpan(
children: [
TextSpan(
text: action.date.toString().split(" ").last,
style: TextStyle(
fontSize: 12,
fontFeatures: [FontFeature.tabularFigures()],
fontFamily: "monospace",
fontFamilyFallback: <String>["Courier"],
child: GestureDetector(
onLongPress: () {
setState(() {
_actions = [];
});
},
child: ListView(
physics: const NeverScrollableScrollPhysics(),
controller: _scrollController,
shrinkWrap: true,
reverse: true,
children:
_actions
.map(
(action) => Text.rich(
TextSpan(
children: [
TextSpan(
text: action.date.toString().split(" ").last,
style: TextStyle(
fontSize: 12,
fontFeatures: [FontFeature.tabularFigures()],
fontFamily: "monospace",
fontFamilyFallback: <String>["Courier"],
),
),
),
TextSpan(
text: " ${action.entry}",
style: TextStyle(
fontSize: 12,
fontFeatures: [FontFeature.tabularFigures()],
fontWeight: FontWeight.bold,
TextSpan(
text: " ${action.entry}",
style: TextStyle(
fontSize: 12,
fontFeatures: [FontFeature.tabularFigures()],
fontWeight: FontWeight.bold,
),
),
),
],
],
),
),
),
)
.toList(),
)
.toList(),
),
),
);
}

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.6.1+7
version: 2.6.3+9
environment:
sdk: ^3.7.0