Compare commits

...

6 Commits

Author SHA1 Message Date
Jonas Bark
01744c258e attempt to improve UX 2025-11-08 22:45:25 +01:00
Jonas Bark
231aadbc27 detect media key source - don't handle it when coming from phone #110 2025-11-08 22:31:12 +01:00
Jonas Bark
a806a628bd Merge remote-tracking branch 'origin/main' 2025-11-08 20:18:58 +01:00
Jonas Bark
c529fee1fa fix execution on Web 2025-11-08 20:18:42 +01:00
jonasbark
c36a0252e6 Aktualisieren von WINDOWS_STORE_VERSION.txt 2025-11-08 15:58:21 +01:00
Jonas Bark
66486ec38e make Di2 custom keymap requirement clearer #170 2025-11-08 12:55:05 +01:00
7 changed files with 158 additions and 140 deletions

View File

@@ -1 +1 @@
3.3.0
3.4.0

View File

@@ -10,6 +10,7 @@ import android.graphics.Rect
import android.media.AudioManager
import android.os.Build
import android.util.Log
import android.view.InputDevice
import android.view.KeyEvent
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityEvent
@@ -69,7 +70,7 @@ class AccessibilityService : AccessibilityService(), Listener {
}
override fun onKeyEvent(event: KeyEvent): Boolean {
if (!Observable.ignoreHidDevices) {
if (!Observable.ignoreHidDevices && isBleRemote(event)) {
// Handle media and volume keys from HID devices here
Log.d(
"AccessibilityService",
@@ -87,6 +88,15 @@ class AccessibilityService : AccessibilityService(), Listener {
}
}
private fun isBleRemote(event: KeyEvent): Boolean {
val dev = InputDevice.getDevice(event.deviceId) ?: return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
dev.isExternal
} else {
true
}
}
override fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) {
val gestureBuilder = GestureDescription.Builder()
val path = Path()

View File

@@ -143,9 +143,11 @@ class WhooshLink {
}
bool isCompatible(Target target) {
return switch (target) {
Target.thisDevice => Platform.isAndroid || Platform.isWindows,
_ => true,
};
return kIsWeb
? false
: switch (target) {
Target.thisDevice => Platform.isAndroid || Platform.isWindows,
_ => true,
};
}
}

View File

@@ -2,7 +2,6 @@ import 'dart:typed_data';
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/bluetooth/messages/notification.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/keymap/apps/custom_app.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
@@ -25,10 +24,6 @@ class ShimanoDi2 extends BluetoothDevice {
);
await UniversalBle.subscribeIndications(device.deviceId, service.uuid, characteristic.uuid);
if (actionHandler.supportedApp is! CustomApp) {
actionStreamInternal.add(LogNotification('Use a custom keymap to support ${scanResult.name}'));
}
}
final _lastButtons = <int, int>{};
@@ -82,12 +77,18 @@ class ShimanoDi2 extends BluetoothDevice {
@override
Widget showInformation(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
super.showInformation(context),
Text(
'Make sure to set your Di2 buttons to D-Fly channels in the Shimano E-TUBE app.',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
if (actionHandler.supportedApp is! CustomApp)
Text(
'Use a custom keymap to support ${scanResult.name}',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
);
}

View File

@@ -12,6 +12,7 @@ import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/desktop.dart';
import 'package:swift_control/utils/keymap/apps/my_whoosh.dart';
import 'package:swift_control/utils/keymap/manager.dart';
import 'package:swift_control/utils/requirements/multi.dart';
import 'package:swift_control/utils/requirements/zwift.dart';
import 'package:swift_control/widgets/beta_pill.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
@@ -334,7 +335,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
if (connection.remoteDevices.isNotEmpty ||
actionHandler is RemoteActions ||
whooshLink.isCompatible(settings.getLastTarget()!) ||
whooshLink.isCompatible(settings.getLastTarget() ?? Target.thisDevice) ||
actionHandler.supportedApp?.supportsZwiftEmulation == true)
Container(
margin: const EdgeInsets.only(bottom: 8.0),
@@ -392,125 +393,129 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
'Customize ${settings.getTrainerApp()?.name} on ${settings.getLastTarget()?.title}',
style: Theme.of(context).textTheme.titleMedium,
),
),
Card(
child: Padding(
padding: EdgeInsets.only(
left: 16.0,
right: 16,
top: 16,
bottom: canVibrate ? 0 : 12,
if (!kIsWeb) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
'Customize ${settings.getTrainerApp()?.name} on ${settings.getLastTarget()?.title}',
style: Theme.of(context).textTheme.titleMedium,
),
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
spacing: 8,
children: [
Expanded(
child: DropdownMenu<SupportedApp?>(
controller: controller,
dropdownMenuEntries: [
..._getAllApps().map(
(app) => DropdownMenuEntry<SupportedApp>(
value: app,
label: app.name,
labelWidget: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(app.name),
if (app is CustomApp) BetaPill(text: 'CUSTOM'),
],
),
Card(
child: Padding(
padding: EdgeInsets.only(
left: 16.0,
right: 16,
top: 16,
bottom: canVibrate ? 0 : 12,
),
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
spacing: 8,
children: [
Expanded(
child: DropdownMenu<SupportedApp?>(
controller: controller,
dropdownMenuEntries: [
..._getAllApps().map(
(app) => DropdownMenuEntry<SupportedApp>(
value: app,
label: app.name,
labelWidget: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(app.name),
if (app is CustomApp) BetaPill(text: 'CUSTOM'),
],
),
),
),
),
DropdownMenuEntry(
value: CustomApp(profileName: 'New'),
label: 'Create new keymap',
labelWidget: Text('Create new keymap'),
leadingIcon: Icon(Icons.add),
),
],
label: Text('Select Keymap'),
onSelected: (app) async {
if (app == null) {
return;
} else if (app.name == 'New') {
final profileName = await KeymapManager().showNewProfileDialog(context);
if (profileName != null && profileName.isNotEmpty) {
final customApp = CustomApp(profileName: profileName);
actionHandler.init(customApp);
await settings.setKeyMap(customApp);
controller.text = profileName;
DropdownMenuEntry(
value: CustomApp(profileName: 'New'),
label: 'Create new keymap',
labelWidget: Text('Create new keymap'),
leadingIcon: Icon(Icons.add),
),
],
label: Text('Select Keymap'),
onSelected: (app) async {
if (app == null) {
return;
} else if (app.name == 'New') {
final profileName = await KeymapManager().showNewProfileDialog(context);
if (profileName != null && profileName.isNotEmpty) {
final customApp = CustomApp(profileName: profileName);
actionHandler.init(customApp);
await settings.setKeyMap(customApp);
controller.text = profileName;
setState(() {});
}
} else {
controller.text = app.name ?? '';
actionHandler.supportedApp = app;
await settings.setKeyMap(app);
setState(() {});
}
} else {
controller.text = app.name ?? '';
actionHandler.supportedApp = app;
await settings.setKeyMap(app);
setState(() {});
}
},
initialSelection: actionHandler.supportedApp,
hintText: 'Select your Keymap',
),
),
Row(
children: [
KeymapManager().getManageProfileDialog(
context,
actionHandler.supportedApp is CustomApp ? actionHandler.supportedApp?.name : null,
onDone: () {
setState(() {});
controller.text = actionHandler.supportedApp?.name ?? '';
},
initialSelection: actionHandler.supportedApp,
hintText: 'Select your Keymap',
),
],
),
Row(
children: [
KeymapManager().getManageProfileDialog(
context,
actionHandler.supportedApp is CustomApp
? actionHandler.supportedApp?.name
: null,
onDone: () {
setState(() {});
controller.text = actionHandler.supportedApp?.name ?? '';
},
),
],
),
],
),
if (actionHandler.supportedApp is! CustomApp)
Text(
'Customize the keymap if you experience any issues (e.g. wrong keyboard output, or misaligned touch placements)',
style: TextStyle(fontSize: 12),
),
if (actionHandler.supportedApp != null && connection.controllerDevices.isNotEmpty)
KeymapExplanation(
key: Key(actionHandler.supportedApp!.keymap.runtimeType.toString()),
keymap: actionHandler.supportedApp!.keymap,
onUpdate: () {
setState(() {});
controller.text = actionHandler.supportedApp?.name ?? '';
if (actionHandler.supportedApp is CustomApp) {
settings.setKeyMap(actionHandler.supportedApp!);
}
},
),
if (canVibrate) ...[
SwitchListTile(
title: Text('Enable vibration feedback when shifting gears'),
value: settings.getVibrationEnabled(),
contentPadding: EdgeInsets.zero,
onChanged: (value) async {
await settings.setVibrationEnabled(value);
setState(() {});
},
),
],
),
if (actionHandler.supportedApp is! CustomApp)
Text(
'Customize the keymap if you experience any issues (e.g. wrong keyboard output, or misaligned touch placements)',
style: TextStyle(fontSize: 12),
),
if (actionHandler.supportedApp != null && connection.controllerDevices.isNotEmpty)
KeymapExplanation(
key: Key(actionHandler.supportedApp!.keymap.runtimeType.toString()),
keymap: actionHandler.supportedApp!.keymap,
onUpdate: () {
setState(() {});
controller.text = actionHandler.supportedApp?.name ?? '';
if (actionHandler.supportedApp is CustomApp) {
settings.setKeyMap(actionHandler.supportedApp!);
}
},
),
if (canVibrate) ...[
SwitchListTile(
title: Text('Enable vibration feedback when shifting gears'),
value: settings.getVibrationEnabled(),
contentPadding: EdgeInsets.zero,
onChanged: (value) async {
await settings.setVibrationEnabled(value);
setState(() {});
},
),
],
],
),
),
),
),
],
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),

View File

@@ -91,19 +91,19 @@ enum Target {
icon: Icons.devices,
),
iOS(
title: 'iPhone / iPad / Apple TV',
title: 'another iPhone / iPad / Apple TV',
icon: Icons.settings_remote_outlined,
),
android(
title: 'Android Device',
title: 'another Android Device',
icon: Icons.settings_remote_outlined,
),
macOS(
title: 'Mac',
title: 'another Mac',
icon: Icons.settings_remote_outlined,
),
windows(
title: 'Windows PC',
title: 'another Windows PC',
icon: Icons.settings_remote_outlined,
);
@@ -136,13 +136,13 @@ enum Target {
'Due to platform restrictions only controlling ${app?.name ?? 'the Trainer app'} on other devices is supported.',
Target.thisDevice => 'Run ${app?.name ?? 'the Trainer app'} on this device.',
Target.iOS =>
'Run ${app?.name ?? 'the Trainer app'} on your Apple device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
'Run ${app?.name ?? 'the Trainer app'} on an Apple device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
Target.android =>
'Run ${app?.name ?? 'the Trainer app'} on your Android device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
'Run ${app?.name ?? 'the Trainer app'} on an Android device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
Target.macOS =>
'Run ${app?.name ?? 'the Trainer app'} on your Mac and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
'Run ${app?.name ?? 'the Trainer app'} on a Mac and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
Target.windows =>
'Run ${app?.name ?? 'the Trainer app'} on your Windows PC and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
'Run ${app?.name ?? 'the Trainer app'} on a Windows PC and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
};
}
@@ -232,6 +232,9 @@ class TargetRequirement extends PlatformRequirement {
whooshLink.stopServer();
}
settings.setTrainerApp(selectedApp!);
if (settings.getLastTarget() == null && Target.thisDevice.isCompatible) {
await settings.setLastTarget(Target.thisDevice);
}
if (actionHandler.supportedApp == null ||
(actionHandler.supportedApp is! CustomApp && selectedApp is! CustomApp)) {
actionHandler.init(selectedApp);
@@ -251,7 +254,7 @@ class TargetRequirement extends PlatformRequirement {
value: target,
label: target.title,
enabled: target.isCompatible,
trailingIcon: Icon(target.icon),
leadingIcon: Icon(target.icon),
labelWidget: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
@@ -259,23 +262,19 @@ class TargetRequirement extends PlatformRequirement {
children: [
Row(
children: [
Text(target.title, style: TextStyle(fontWeight: FontWeight.bold)),
Text(
target.title,
style: TextStyle(
fontWeight: target == Target.thisDevice && target.isCompatible ? FontWeight.bold : null,
),
),
if (target.isBeta) BetaPill(),
],
),
Text(
target.getDescription(settings.getTrainerApp()),
style: TextStyle(fontSize: 12, color: Colors.grey),
style: TextStyle(fontSize: 10, color: Colors.grey),
),
if (target == Target.thisDevice)
Container(
margin: EdgeInsets.only(top: 12),
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).dividerColor,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
@@ -283,6 +282,7 @@ class TargetRequirement extends PlatformRequirement {
}).toList(),
hintText: 'Select Target device',
initialSelection: settings.getLastTarget(),
enabled: settings.getTrainerApp() != null,
onSelected: (target) async {
if (target != null) {
await settings.setLastTarget(target);

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: 3.4.0+37
version: 3.4.0+38
environment:
sdk: ^3.9.0