Compare commits

..

19 Commits

Author SHA1 Message Date
jonasbark
1e11d28765 Merge pull request #71 from jonasbark/copilot/fix-64
Fix Windows mouse clicks at wrong location due to display scaling
2025-09-17 08:49:53 +02:00
copilot-swe-agent[bot]
7ee9bc43a0 Fix changelog date to 2025-09-17
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-17 06:49:09 +00:00
copilot-swe-agent[bot]
372085ec0e Update version to 2.4.0+1 and add changelog entry
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-17 06:46:52 +00:00
copilot-swe-agent[bot]
e758b35837 Fix Windows mouse click scaling for high DPI displays
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-17 06:32:42 +00:00
copilot-swe-agent[bot]
dee7b86120 Initial plan 2025-09-17 06:28:06 +00:00
Jonas Bark
b3ec7e7a3a funding 2025-09-16 20:08:51 +02:00
Jonas Bark
bbd01d023a - Show an overview of the keymap bindings
- Allow customizing an existing keymap
2025-09-16 10:32:09 +02:00
Jonas Bark
36282c9fa9 better donate options 2025-09-16 08:59:50 +02:00
jonasbark
daea07c409 Clarify iOS not being supported 2025-09-15 08:08:07 +02:00
jonasbark
49d7445d0e Aktualisieren von README.md 2025-09-11 21:14:32 +02:00
jonasbark
9bb0e5616a Aktualisieren von pubspec.yaml 2025-09-11 19:27:47 +02:00
jonasbark
7e98f595ee Aktualisieren von CHANGELOG.md 2025-09-11 19:27:18 +02:00
Jonas Bark
a9fdc4b16e attempt to add support for Zwift Click v2 2025-09-10 17:40:14 +02:00
Jonas Bark
c06819b502 attempt to add support for Zwift Click v2 2025-09-10 08:42:55 +02:00
Jonas Bark
969faca658 attempt to add support for Zwift Click v2 2025-09-09 09:19:52 +02:00
Jonas Bark
61fbb099e2 actions fix 2025-09-08 16:55:28 +02:00
Jonas Bark
fbd6356be0 donate button change 2025-09-08 16:54:23 +02:00
Jonas Bark
1c40455bf3 update readme 2025-09-08 16:42:30 +02:00
Jonas Bark
15129634a6 update some libraries to ensure compatibility with latest Flutter 2025-09-08 16:23:20 +02:00
17 changed files with 363 additions and 121 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,2 @@
github: [jonasbark]
open_collective: jonas-bark1
custom: ["https://paypal.me/boni"]
custom: ["https://paypal.me/boni", "https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200"]

View File

@@ -1,4 +1,15 @@
### 2.2.0 (2025-01-03)
### 2.4.0+1 (2025-09-17)
- Windows: fix mouse clicks at wrong location due to display scaling (fixes #64)
### 2.4.0 (2025-09-16)
- Show an overview of the keymap bindings
- Allow customizing an existing keymap
- Add more donation options
### 2.3.0 (2025-09-11)
- Add support for latest Zwift Click v2
### 2.2.0 (2025-09-08)
- Add Long Press Mode option for custom keymaps - buttons can now send sustained key presses instead of repeated taps, perfect for movement controls in games (fixes #61)
- Windows: adjust key sending method to improve compatibility with more apps (fixes #62)

View File

@@ -4,7 +4,7 @@
## Description
With SwiftControl you can control your favorite trainer app using your Zwift Click, Zwift Ride or Zwift Play devices. Here's what you can do with it, depending on your configuration:
With SwiftControl you can **control your favorite trainer app** using your Zwift Click, Zwift Ride or Zwift Play devices. Here's what you can do with it, depending on your configuration:
- Virtual Gear shifting
- Steering / turning
- adjust workout intensity
@@ -30,28 +30,29 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
## Supported Devices
- Zwift Click
- Zwift Click v2
- Zwift Ride
- Zwift Play
## Supported Platforms
- Android
- App is losing connection over time? Read about how to [keep the app alive](https://dontkillmyapp.com/).
- macOS
- Windows
- make sure you have installed the "[Microsoft Visual C++ Runtime libraries](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)"
- Windows may flag the app as virus. I think it does so because the app does control the mouse and keyboard.
- Bluetooth connection unstable? You may need to use an [external Bluetooth adapter](https://github.com/jonasbark/swiftcontrol/issues/14#issuecomment-3193839509).
- [Web](https://jonasbark.github.io/swiftcontrol/) (you won't be able to do much)
- NOT SUPPORTED: iOS (iPhone, iPad) as Apple does not provide any way to simulate touches or keyboard events
## Troubleshooting
- 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/).
- **Windows** bluetooth connection unstable? You may need to use an [external Bluetooth adapter](https://github.com/jonasbark/swiftcontrol/issues/14#issuecomment-3193839509).
## How does it work?
The app connects to your Zwift device automatically.
The app connects to your Zwift device automatically. It does not connect to your trainer itself.
- When using Android a "click" on a certain part of the screen is simulated to trigger the action.
- When using Android a touch on a certain part of the screen is simulated to trigger the action.
- When using macOS or Windows a keyboard or mouse click is used to trigger the action.
- there are predefined Keymaps for MyWhoosh and indieVelo / Training Peaks
- there are predefined Keymaps for MyWhoosh, indieVelo / Training Peaks, and others
- you can also create your own Keymaps for any other app
- you can also use the mouse to click on a certain part of the screen, or use keyboard shortcuts

View File

@@ -4,6 +4,7 @@
#include <windows.h>
#include <psapi.h>
#include <string.h>
#include <flutter_windows.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
@@ -126,8 +127,18 @@ void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
y = std::get<double>(it_y->second);
}
// Get the monitor containing the target point and its DPI
const POINT target_point = {static_cast<LONG>(x), static_cast<LONG>(y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
// Scale the coordinates according to the DPI scaling
int scaled_x = static_cast<int>(x * scale_factor);
int scaled_y = static_cast<int>(y * scale_factor);
// Move the mouse to the specified coordinates
SetCursorPos(static_cast<int>(x), static_cast<int>(y));
SetCursorPos(scaled_x, scaled_y);
// Prepare input for mouse down and up
INPUT input = {0};

View File

@@ -9,7 +9,7 @@ class BleUuid {
}
class Constants {
static const ZWIFT_MANUFACTURER_ID = 2378; // Zwift, Inc
static const ZWIFT_MANUFACTURER_ID = 2378; // Zwift, Inc => 0x094A
// Zwift Play = RC1
static const RC1_LEFT_SIDE = 0x03;
@@ -22,6 +22,11 @@ class Constants {
// Zwift Click = BC1
static const BC1 = 0x09;
// Zwift Click v2 Right (unconfirmed)
static const CLICK_V2_RIGHT_SIDE = 0x0A;
// Zwift Click v2 Right (unconfirmed)
static const CLICK_V2_LEFT_SIDE = 0x0B;
static final RIDE_ON = Uint8List.fromList([0x52, 0x69, 0x64, 0x65, 0x4f, 0x6e]);
static final VIBRATE_PATTERN = Uint8List.fromList([0x12, 0x12, 0x08, 0x0A, 0x06, 0x08, 0x02, 0x10, 0x00, 0x18]);
@@ -46,6 +51,8 @@ class Constants {
enum DeviceType {
click,
clickV2Right,
clickV2Left,
playLeft,
playRight,
rideRight,
@@ -61,6 +68,10 @@ enum DeviceType {
switch (data) {
case Constants.BC1:
return DeviceType.click;
case Constants.CLICK_V2_RIGHT_SIDE:
return DeviceType.clickV2Right;
case Constants.CLICK_V2_LEFT_SIDE:
return DeviceType.clickV2Left;
case Constants.RC1_LEFT_SIDE:
return DeviceType.playLeft;
case Constants.RC1_RIGHT_SIDE:

View File

@@ -34,11 +34,15 @@ class Connection {
if (_lastScanResult.none((e) => e.deviceId == result.deviceId)) {
_lastScanResult.add(result);
final scanResult = BaseDevice.fromScanResult(result);
_actionStreams.add(
LogNotification('Found new device: ${result.name ?? scanResult?.runtimeType ?? result.deviceId}'),
);
if (scanResult != null) {
_actionStreams.add(LogNotification('Found new device: ${scanResult.runtimeType}'));
_addDevices([scanResult]);
} else {
final manufacturerData = result.manufacturerDataList;
final data =
manufacturerData.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)?.payload;
_actionStreams.add(LogNotification('Found unknown device with identifier: ${data?.firstOrNull}'));
}
}
};

View File

@@ -5,6 +5,7 @@ import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:swift_control/bluetooth/ble.dart';
import 'package:swift_control/bluetooth/devices/zwift_click.dart';
import 'package:swift_control/bluetooth/devices/zwift_clickv2.dart';
import 'package:swift_control/bluetooth/devices/zwift_play.dart';
import 'package:swift_control/bluetooth/devices/zwift_ride.dart';
import 'package:swift_control/main.dart';
@@ -41,7 +42,7 @@ abstract class BaseDevice {
//'Zwift Ride' => ZwiftRide(scanResult), special case for Zwift Ride: we must only connect to the left controller
// https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/
'Zwift Play' => ZwiftPlay(scanResult),
'Zwift Click' => ZwiftClick(scanResult),
//'Zwift Click' => ZwiftClick(scanResult), special case for Zwift Click v2: we must only connect to the left controller
_ => null,
};
@@ -61,8 +62,10 @@ abstract class BaseDevice {
DeviceType.click => ZwiftClick(scanResult),
DeviceType.playRight => ZwiftPlay(scanResult),
DeviceType.playLeft => ZwiftPlay(scanResult),
//DeviceType.rideRight => ZwiftRide(scanResult), // see comment above
DeviceType.rideLeft => ZwiftRide(scanResult),
//DeviceType.rideRight => ZwiftRide(scanResult), // see comment above
DeviceType.clickV2Left => ZwiftClickV2(scanResult),
//DeviceType.clickV2Right => ZwiftClickV2(scanResult), // see comment above
_ => null,
};
}
@@ -125,25 +128,15 @@ abstract class BaseDevice {
throw Exception('Characteristics not found');
}
await UniversalBle.setNotifiable(
device.deviceId,
customService.uuid,
asyncCharacteristic.uuid,
BleInputProperty.notification,
);
await UniversalBle.setNotifiable(
device.deviceId,
customService.uuid,
syncTxCharacteristic.uuid,
BleInputProperty.indication,
);
await UniversalBle.subscribeNotifications(device.deviceId, customService.uuid, asyncCharacteristic.uuid);
await UniversalBle.subscribeIndications(device.deviceId, customService.uuid, syncTxCharacteristic.uuid);
await _setupHandshake();
}
Future<void> _setupHandshake() async {
if (supportsEncryption) {
await UniversalBle.writeValue(
await UniversalBle.write(
device.deviceId,
customServiceId,
syncRxCharacteristic!.uuid,
@@ -152,15 +145,15 @@ abstract class BaseDevice {
...Constants.REQUEST_START,
...zapEncryption.localKeyProvider.getPublicKeyBytes(),
]),
BleOutputProperty.withoutResponse,
withoutResponse: true,
);
} else {
await UniversalBle.writeValue(
await UniversalBle.write(
device.deviceId,
customServiceId,
syncRxCharacteristic!.uuid,
Constants.RIDE_ON,
BleOutputProperty.withoutResponse,
withoutResponse: true,
);
}
}
@@ -314,12 +307,12 @@ abstract class BaseDevice {
Future<void> _vibrate() async {
final vibrateCommand = Uint8List.fromList([...Constants.VIBRATE_PATTERN, 0x20]);
await UniversalBle.writeValue(
await UniversalBle.write(
device.deviceId,
customServiceId,
syncRxCharacteristic!.uuid,
supportsEncryption ? zapEncryption.encrypt(vibrateCommand) : vibrateCommand,
BleOutputProperty.withoutResponse,
withoutResponse: true,
);
}

View File

@@ -0,0 +1,5 @@
import 'package:swift_control/bluetooth/devices/zwift_ride.dart';
class ZwiftClickV2 extends ZwiftRide {
ZwiftClickV2(super.scanResult);
}

View File

@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/pages/touch_area.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
import 'package:swift_control/widgets/logviewer.dart';
import 'package:swift_control/widgets/title.dart';
@@ -57,7 +58,7 @@ class _DevicePageState extends State<DevicePage> {
actions: buildMenuButtons(),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
body: SingleChildScrollView(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -70,80 +71,92 @@ class _DevicePageState extends State<DevicePage> {
),
Divider(color: Theme.of(context).colorScheme.primary, height: 30),
if (!kIsWeb)
Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownMenu<SupportedApp>(
controller: controller,
dropdownMenuEntries:
SupportedApp.supportedApps
.map(
(app) => DropdownMenuEntry<SupportedApp>(
value: app,
label: app.name,
trailingIcon: IconButton(
icon: Icon(Icons.info_outline),
onPressed: () {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text(app.name),
content: SelectableText(app.keymap.toString()),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
},
Flex(
mainAxisAlignment: MainAxisAlignment.start,
direction: MediaQuery.sizeOf(context).width > 600 ? Axis.horizontal : Axis.vertical,
spacing: 8,
children: [
DropdownMenu<SupportedApp>(
controller: controller,
dropdownMenuEntries:
SupportedApp.supportedApps
.map((app) => DropdownMenuEntry<SupportedApp>(value: app, label: app.name))
.toList(),
label: Text('Select Keymap'),
onSelected: (app) async {
if (app == null) {
return;
}
controller.text = app.name ?? '';
actionHandler.supportedApp = app;
settings.setApp(app);
setState(() {});
if (app is! CustomApp && !kIsWeb && (Platform.isMacOS || Platform.isWindows)) {
_snackBarMessengerKey.currentState!.showSnackBar(
SnackBar(
content: Text(
'Customize the keymap if you experience any issues (e.g. wrong keyboard output)',
),
),
)
.toList(),
label: Text('Select Keymap'),
onSelected: (app) async {
if (app == null) {
return;
}
controller.text = app.name ?? '';
actionHandler.supportedApp = app;
settings.setApp(app);
setState(() {});
if (app is! CustomApp && !kIsWeb && (Platform.isMacOS || Platform.isWindows)) {
_snackBarMessengerKey.currentState!.showSnackBar(
SnackBar(
content: Text(
'Use Custom keymap if you experience any issues (e.g. wrong keyboard output)',
),
),
);
}
},
initialSelection: actionHandler.supportedApp,
hintText: 'Select your Keymap',
),
);
}
},
initialSelection: actionHandler.supportedApp,
hintText: 'Select your Keymap',
),
if (actionHandler.supportedApp is CustomApp)
ElevatedButton(
onPressed: () async {
final result = await Navigator.of(
context,
).push<bool>(MaterialPageRoute(builder: (_) => TouchAreaSetupPage()));
if (result == true && actionHandler.supportedApp is CustomApp) {
settings.setApp(actionHandler.supportedApp!);
}
ElevatedButton(
onPressed: () async {
if (actionHandler.supportedApp! is! CustomApp) {
final customApp = CustomApp();
actionHandler.supportedApp!.keymap.keyPairs.forEachIndexed((pair, index) {
pair.buttons.forEachIndexed((button, indexB) {
customApp.setKey(
button,
physicalKey: pair.physicalKey!,
logicalKey: pair.logicalKey,
isLongPress: pair.isLongPress,
touchPosition:
pair.touchPosition != Offset.zero
? pair.touchPosition
: Offset(((indexB + 1)) * 100, 200 + (index * 100)),
);
});
});
actionHandler.supportedApp = customApp;
settings.setApp(customApp);
}
final result = await Navigator.of(
context,
).push<bool>(MaterialPageRoute(builder: (_) => TouchAreaSetupPage()));
if (result == true && actionHandler.supportedApp is CustomApp) {
settings.setApp(actionHandler.supportedApp!);
}
setState(() {});
},
child: Text('Customize Keymap'),
),
],
),
if (actionHandler.supportedApp != null)
KeymapExplanation(
key: Key(actionHandler.supportedApp!.keymap.runtimeType.toString()),
keymap: actionHandler.supportedApp!.keymap,
onUpdate: () {
setState(() {});
controller.text = actionHandler.supportedApp?.name ?? '';
},
child: Text('Customize Keymap'),
),
],
),
Expanded(child: LogViewer()),
SizedBox(height: 800, child: LogViewer()),
],
),
),

View File

@@ -350,7 +350,7 @@ class _TouchDot extends StatelessWidget {
? Icons.music_note_outlined
: keyPair.physicalKey != null
? Icons.keyboard_alt_outlined
: Icons.add,
: Icons.touch_app_outlined,
),
),

View File

@@ -43,6 +43,7 @@ class CustomApp extends SupportedApp {
required PhysicalKeyboardKey physicalKey,
required LogicalKeyboardKey? logicalKey,
bool isLongPress = false,
Offset? touchPosition,
}) {
// set the key for the zwift button
final keyPair = keymap.getKeyPair(zwiftButton);
@@ -50,13 +51,17 @@ class CustomApp extends SupportedApp {
keyPair.physicalKey = physicalKey;
keyPair.logicalKey = logicalKey;
keyPair.isLongPress = isLongPress;
keyPair.touchPosition = touchPosition ?? Offset.zero;
} else {
keymap.keyPairs.add(KeyPair(
buttons: [zwiftButton],
physicalKey: physicalKey,
logicalKey: logicalKey,
isLongPress: isLongPress,
));
keymap.keyPairs.add(
KeyPair(
buttons: [zwiftButton],
physicalKey: physicalKey,
logicalKey: logicalKey,
isLongPress: isLongPress,
touchPosition: touchPosition ?? Offset.zero,
),
);
}
}
}

View File

@@ -71,6 +71,7 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
_pressedButton!,
physicalKey: _pressedKey!.physicalKey,
logicalKey: _pressedKey!.logicalKey,
touchPosition: widget.keyPair?.touchPosition,
);
}
});

View File

@@ -0,0 +1,148 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/keymap/keymap.dart';
class KeymapExplanation extends StatelessWidget {
final Keymap keymap;
final VoidCallback onUpdate;
const KeymapExplanation({super.key, required this.keymap, required this.onUpdate});
@override
Widget build(BuildContext context) {
final keyboardGroups = keymap.keyPairs
.filter((e) => e.physicalKey != null)
.groupBy((element) => '${element.physicalKey}-${element.isLongPress}');
final touchGroups = keymap.keyPairs
.filter((e) => e.physicalKey == null && e.touchPosition != Offset.zero)
.groupBy((element) => '${element.touchPosition}-${element.isLongPress}');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
if (keymap.keyPairs.isEmpty)
Text('No key mappings found. Please customize the keymap.')
else
Table(
border: TableBorder.all(color: Theme.of(context).colorScheme.primaryContainer),
children: [
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Text(
'Button on your ${connection.devices.firstOrNull?.device.name ?? connection.devices.firstOrNull?.runtimeType}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.all(6),
child: Text(
'Action',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
for (final pair in keyboardGroups.entries) ...[
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final keyPair in pair.value)
for (final button in keyPair.buttons)
IntrinsicWidth(child: _KeyWidget(label: button.name.splitByUpperCase())),
],
),
),
Padding(
padding: const EdgeInsets.all(6),
child: Row(
spacing: 8,
children: [
Icon(Icons.keyboard, size: 16),
_KeyWidget(label: pair.value.first.logicalKey?.keyLabel ?? ''),
if (pair.value.first.isLongPress) Text('using long press'),
],
),
),
],
),
],
for (final pair in touchGroups.entries) ...[
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Row(
spacing: 8,
children: [
for (final keyPair in pair.value)
for (final button in keyPair.buttons) _KeyWidget(label: button.name.splitByUpperCase()),
],
),
),
Padding(
padding: const EdgeInsets.all(6),
child: Row(
spacing: 8,
children: [
Icon(Icons.touch_app, size: 16),
_KeyWidget(
label:
'x: ${pair.value.first.touchPosition.dx.toInt()}, y: ${pair.value.first.touchPosition.dy.toInt()}',
),
if (pair.value.first.isLongPress) Text('using long press'),
],
),
),
],
),
],
],
),
],
);
}
}
class _KeyWidget extends StatelessWidget {
final String label;
const _KeyWidget({super.key, required this.label});
@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,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
);
}
}
extension on String {
String splitByUpperCase() {
return replaceAllMapped(RegExp(r'([a-z])([A-Z])'), (match) => '${match.group(1)} ${match.group(2)}').capitalize();
}
}

View File

@@ -1,17 +1,41 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../pages/device.dart';
List<Widget> buildMenuButtons() {
return [
TextButton(
onPressed: () {
launchUrlString('https://paypal.me/boni');
PopupMenuButton(
itemBuilder: (BuildContext context) {
return [
PopupMenuItem(
child: Text('via Credit Card, Google Pay, Apple Pay and others'),
onTap: () {
final currency = NumberFormat.simpleCurrency(locale: kIsWeb ? 'de_DE' : Platform.localeName);
final link = switch (currency.currencyName) {
'USD' => 'https://donate.stripe.com/8x24gzc5c4ZE3VJdt36J201',
_ => 'https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200',
};
launchUrlString(link);
},
),
PopupMenuItem(
child: Text('via PayPal'),
onTap: () {
launchUrlString('https://paypal.me/boni');
},
),
];
},
child: Text('Donate ♥'),
icon: Text('Donate ♥', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
),
SizedBox(width: 8),
const MenuButton(),
SizedBox(width: 8),
];
@@ -37,6 +61,13 @@ class MenuButton extends StatelessWidget {
),
),
PopupMenuItem(child: PopupMenuDivider()),
PopupMenuItem(
child: Text('Continue'),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (c) => DevicePage()));
},
),
PopupMenuItem(child: PopupMenuDivider()),
],
PopupMenuItem(
child: Text('Feedback'),

View File

@@ -91,7 +91,7 @@ class _AppTitleState extends State<AppTitle> {
Text('SwiftControl'),
if (_packageInfoValue != null)
Text(
'v${_packageInfoValue!.version}',
'v${_packageInfoValue!.version}+${_packageInfoValue!.buildNumber}',
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"], fontSize: 12),
)
else

View File

@@ -249,10 +249,10 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: d59eeafd6df92174b1d5f68fc9d66634c97ce2e7cfe2293476236547bb19bbbd
sha256: a9966c850de5e445331b854fa42df96a8020066d67f125a5964cbc6556643f68
url: "https://pub.dev"
source: hosted
version: "19.0.0"
version: "19.4.1"
flutter_local_notifications_linux:
dependency: transitive
description:
@@ -265,18 +265,18 @@ packages:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c"
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "9.1.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -391,6 +391,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
json_annotation:
dependency: transitive
description:

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.2.0
version: 2.4.0+1
environment:
sdk: ^3.7.0
@@ -11,8 +11,9 @@ dependencies:
sdk: flutter
url_launcher: ^6.3.1
flutter_local_notifications: ^19.0.0
flutter_local_notifications: ^19.4.1
universal_ble: ^0.21.1
intl: any
protobuf: ^3.1.0
permission_handler: ^11.4.0
dartx: any