mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc63f693f0 | ||
|
|
455db754d8 | ||
|
|
cbef8fc044 | ||
|
|
d8e45f849a |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user