mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
MyWhoosh Link implementation #2
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
### 3.2.0 (2025-10-22)
|
||||
- a brand-new way of controlling MyWhoosh:
|
||||
- device pairing no longer required as mouse emulation is no longer needed
|
||||
- SwiftControl can now stay in the background
|
||||
- more devices can be controlled
|
||||
- do more, such as define Emotes, Camera angles and steering
|
||||
|
||||
### 3.1.0 (2025-10-17)
|
||||
- new app icon
|
||||
- adjusted MyWhoosh keyboard navigation mapping (thanks @bin101)
|
||||
|
||||
1
INSTRUCTIONS_ANDROID.md
Normal file
1
INSTRUCTIONS_ANDROID.md
Normal file
@@ -0,0 +1 @@
|
||||
Instructions will be added soon
|
||||
1
INSTRUCTIONS_IOS.md
Normal file
1
INSTRUCTIONS_IOS.md
Normal file
@@ -0,0 +1 @@
|
||||
Instructions will be added soon
|
||||
1
INSTRUCTIONS_MACOS.md
Normal file
1
INSTRUCTIONS_MACOS.md
Normal file
@@ -0,0 +1 @@
|
||||
Instructions will be added soon
|
||||
1
INSTRUCTIONS_WINDOWS.md
Normal file
1
INSTRUCTIONS_WINDOWS.md
Normal file
@@ -0,0 +1 @@
|
||||
Instructions will be added soon
|
||||
@@ -36,3 +36,15 @@ switch the setting to None, then back to Single-Tap and it should work again
|
||||
|
||||
## SwiftControl crashes on Windows when searching for the device
|
||||
You're probably running into [this](https://github.com/jonasbark/swiftcontrol/issues/70) issue. Disconnect your controller device (e.g. Zwift Play) from Windows Bluetooth settings.
|
||||
|
||||
## Link requirement for MyWhoosh stuck at "Waiting for MyWhoosh"
|
||||
The same network restrictions apply for SwiftControl as it applies to MyWhoosh Link app. Please verify with the MyWhoosh Link app if connection is possible at all.
|
||||
Here are some instructions that can help:
|
||||
https://mywhoosh.com/troubleshoot/
|
||||
https://www.facebook.com/groups/mywhoosh/posts/1323791068858873/
|
||||
In essence:
|
||||
- your two devices (phone, tablet) need to be on the same WiFi network
|
||||
- on iOS you have to turn off "Private Wi-Fi Address" in the WiFi settings
|
||||
- Limit IP Address Tracking may need to be disabled
|
||||
- mesh networks may not work
|
||||
|
||||
|
||||
@@ -8,9 +8,31 @@ class WhooshLink {
|
||||
Socket? _socket;
|
||||
ServerSocket? _server;
|
||||
|
||||
static final List<InGameAction> supportedActions = [
|
||||
InGameAction.shiftUp,
|
||||
InGameAction.shiftDown,
|
||||
InGameAction.cameraAngle,
|
||||
InGameAction.emote,
|
||||
InGameAction.uturn,
|
||||
InGameAction.steerLeft,
|
||||
InGameAction.steerRight,
|
||||
];
|
||||
|
||||
final ValueNotifier<bool> isConnected = ValueNotifier(false);
|
||||
final ValueNotifier<bool> isStarted = ValueNotifier(false);
|
||||
|
||||
void stopServer() async {
|
||||
if (isStarted.value) {
|
||||
await _socket?.close();
|
||||
await _server?.close();
|
||||
isConnected.value = false;
|
||||
isStarted.value = false;
|
||||
if (kDebugMode) {
|
||||
print('Server stopped.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startServer() async {
|
||||
// Create and bind server socket
|
||||
_server = await ServerSocket.bind(
|
||||
@@ -51,7 +73,7 @@ class WhooshLink {
|
||||
});
|
||||
}
|
||||
|
||||
String sendAction(InGameAction action) {
|
||||
String sendAction(InGameAction action, int? value) {
|
||||
if (!isConnected.value) {
|
||||
return 'Not connected to MyWhoosh.';
|
||||
}
|
||||
@@ -71,13 +93,13 @@ class WhooshLink {
|
||||
InGameAction.cameraAngle => {
|
||||
'MessageType': 'Controls',
|
||||
'InGameControls': {
|
||||
'CameraAngle': '1',
|
||||
'CameraAngle': '$value',
|
||||
},
|
||||
},
|
||||
InGameAction.emote => {
|
||||
'MessageType': 'Controls',
|
||||
'InGameControls': {
|
||||
'Emote': '1',
|
||||
'Emote': '$value',
|
||||
},
|
||||
},
|
||||
InGameAction.uturn => {
|
||||
@@ -86,10 +108,16 @@ class WhooshLink {
|
||||
'UTurn': 'true',
|
||||
},
|
||||
},
|
||||
InGameAction.steering => {
|
||||
InGameAction.steerLeft => {
|
||||
'MessageType': 'Controls',
|
||||
'InGameControls': {
|
||||
'Steering': '0',
|
||||
'Steering': '-1',
|
||||
},
|
||||
},
|
||||
InGameAction.steerRight => {
|
||||
'MessageType': 'Controls',
|
||||
'InGameControls': {
|
||||
'Steering': '1',
|
||||
},
|
||||
},
|
||||
InGameAction.increaseResistance => null,
|
||||
|
||||
@@ -31,12 +31,16 @@ void main() async {
|
||||
}
|
||||
|
||||
enum ConnectionType {
|
||||
unknown,
|
||||
local,
|
||||
remote,
|
||||
link,
|
||||
}
|
||||
|
||||
Future<void> initializeActions(ConnectionType connectionType) async {
|
||||
if (connectionType != ConnectionType.link) {
|
||||
whooshLink.stopServer();
|
||||
}
|
||||
if (kIsWeb) {
|
||||
actionHandler = StubActions();
|
||||
} else if (Platform.isAndroid) {
|
||||
@@ -44,18 +48,21 @@ Future<void> initializeActions(ConnectionType connectionType) async {
|
||||
ConnectionType.local => AndroidActions(),
|
||||
ConnectionType.remote => RemoteActions(),
|
||||
ConnectionType.link => LinkActions(),
|
||||
ConnectionType.unknown => StubActions(),
|
||||
};
|
||||
} else if (Platform.isIOS) {
|
||||
actionHandler = switch (connectionType) {
|
||||
ConnectionType.local => throw UnimplementedError('Local actions are not supported on iOS'),
|
||||
ConnectionType.local => StubActions(),
|
||||
ConnectionType.remote => RemoteActions(),
|
||||
ConnectionType.link => LinkActions(),
|
||||
ConnectionType.unknown => StubActions(),
|
||||
};
|
||||
} else {
|
||||
actionHandler = switch (connectionType) {
|
||||
ConnectionType.local => DesktopActions(),
|
||||
ConnectionType.remote => RemoteActions(),
|
||||
ConnectionType.link => LinkActions(),
|
||||
ConnectionType.unknown => StubActions(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:swift_control/pages/touch_area.dart';
|
||||
import 'package:swift_control/utils/actions/desktop.dart';
|
||||
import 'package:swift_control/utils/actions/link.dart';
|
||||
import 'package:swift_control/utils/keymap/manager.dart';
|
||||
import 'package:swift_control/widgets/beta_pill.dart';
|
||||
import 'package:swift_control/widgets/ingameactions_customizer.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
import 'package:swift_control/widgets/loading_widget.dart';
|
||||
@@ -170,25 +171,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
device.device.name?.screenshot ?? device.runtimeType.toString(),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (device.isBeta)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'BETA',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (device.isBeta) BetaPill(),
|
||||
if (device.batteryLevel != null) ...[
|
||||
Icon(switch (device.batteryLevel!) {
|
||||
>= 80 => Icons.battery_full,
|
||||
@@ -493,7 +476,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
],
|
||||
),
|
||||
if (actionHandler is LinkActions)
|
||||
IngameactionsCustomizer()
|
||||
InGameActionsCustomizer()
|
||||
else if (actionHandler.supportedApp != null)
|
||||
KeymapExplanation(
|
||||
key: Key(actionHandler.supportedApp!.keymap.runtimeType.toString()),
|
||||
|
||||
@@ -158,7 +158,7 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
}
|
||||
|
||||
void _reloadRequirements() {
|
||||
getRequirements(settings.getLastTarget()?.connectionType ?? ConnectionType.local).then((req) {
|
||||
getRequirements(settings.getLastTarget()?.connectionType ?? ConnectionType.unknown).then((req) {
|
||||
_requirements = req;
|
||||
_currentStep = req.indexWhere((req) => !req.status);
|
||||
if (mounted) {
|
||||
|
||||
@@ -11,6 +11,7 @@ class LinkActions extends BaseActions {
|
||||
if (inGameAction == null) {
|
||||
return 'No action defined for button: $action';
|
||||
}
|
||||
return whooshLink.sendAction(inGameAction);
|
||||
final value = settings.getInGameActionForButtonValue(action);
|
||||
return whooshLink.sendAction(inGameAction, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
|
||||
enum InGameAction {
|
||||
shiftUp,
|
||||
shiftDown,
|
||||
navigateLeft,
|
||||
navigateRight,
|
||||
increaseResistance,
|
||||
decreaseResistance,
|
||||
toggleUi,
|
||||
cameraAngle,
|
||||
emote,
|
||||
uturn,
|
||||
steering;
|
||||
shiftUp('Shift Up'),
|
||||
shiftDown('Shift Down'),
|
||||
navigateLeft('Navigate Left'),
|
||||
navigateRight('Navigate Right'),
|
||||
increaseResistance('Increase Resistance'),
|
||||
decreaseResistance('Decrease Resistance'),
|
||||
toggleUi('Toggle UI'),
|
||||
cameraAngle('Change Camera Angle', possibleValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
|
||||
emote('Emote', possibleValues: [1, 2, 3, 4, 5, 6]),
|
||||
uturn('U-Turn'),
|
||||
steerLeft('Steer Left'),
|
||||
steerRight('Steer Right');
|
||||
|
||||
final String title;
|
||||
final List<int>? possibleValues;
|
||||
|
||||
const InGameAction(this.title, {this.possibleValues});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return name.splitByUpperCase();
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/pages/scan.dart';
|
||||
import 'package:swift_control/utils/requirements/platform.dart';
|
||||
import 'package:swift_control/utils/requirements/remote.dart';
|
||||
import 'package:swift_control/widgets/small_progress_indicator.dart';
|
||||
import 'package:swift_control/widgets/beta_pill.dart';
|
||||
import 'package:swift_control/widgets/link.dart';
|
||||
import 'package:swift_control/widgets/scan.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
class KeyboardRequirement extends PlatformRequirement {
|
||||
@@ -84,38 +85,54 @@ class BluetoothScanning extends PlatformRequirement {
|
||||
typedef BoolFunction = bool Function();
|
||||
|
||||
enum Target {
|
||||
thisDevice(title: 'This device', description: 'Trainer app runs on this device', icon: Icons.devices),
|
||||
thisDevice(
|
||||
title: 'This device',
|
||||
description: 'Trainer app runs on this device',
|
||||
icon: Icons.devices,
|
||||
),
|
||||
myWhooshLink(
|
||||
title: 'MyWhoosh Link',
|
||||
description: 'Control MyWhoosh directly on another device',
|
||||
description: 'Control MyWhoosh directly on another device, such as a tablet or a TV',
|
||||
icon: Icons.link,
|
||||
),
|
||||
iPad(
|
||||
title: 'iPad',
|
||||
description: 'Remotely control the trainer app on an iPad',
|
||||
description: 'Remotely control any trainer app on an iPad by acting as a Mouse',
|
||||
icon: Icons.settings_remote_outlined,
|
||||
isBeta: true,
|
||||
),
|
||||
android(
|
||||
title: 'Android Device',
|
||||
description: 'Remotely control the trainer app on an Android device',
|
||||
description: 'Remotely control any trainer app on an Android device',
|
||||
icon: Icons.settings_remote_outlined,
|
||||
isBeta: true,
|
||||
),
|
||||
macOS(
|
||||
title: 'Mac',
|
||||
description: 'Remotely control the trainer app on a Mac',
|
||||
description: 'Remotely control any trainer app on a Mac',
|
||||
icon: Icons.settings_remote_outlined,
|
||||
isBeta: true,
|
||||
),
|
||||
windows(
|
||||
title: 'Windows PC',
|
||||
description: 'Remotely control the trainer app on a Windows PC',
|
||||
description: 'Remotely control any trainer app on a Windows PC',
|
||||
icon: Icons.settings_remote_outlined,
|
||||
isBeta: true,
|
||||
);
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData icon;
|
||||
final bool isBeta;
|
||||
|
||||
const Target({required this.title, required this.description, required this.icon});
|
||||
const Target({required this.title, required this.description, required this.icon, this.isBeta = false});
|
||||
|
||||
bool get isCompatible {
|
||||
return switch (this) {
|
||||
Target.thisDevice => !Platform.isIOS,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
String? get warning {
|
||||
return switch (this) {
|
||||
@@ -165,19 +182,34 @@ class TargetRequirement extends PlatformRequirement {
|
||||
return DropdownMenuEntry(
|
||||
value: target,
|
||||
label: target.title,
|
||||
enabled: target.isCompatible,
|
||||
trailingIcon: Icon(target.icon),
|
||||
labelWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(target.title, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
target == Target.myWhooshLink && Platform.isAndroid
|
||||
? 'Control MyWhoosh directly on this or another device'
|
||||
: target.description,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
if (target == Target.myWhooshLink) Divider(),
|
||||
],
|
||||
labelWidget: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(target.title, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
if (target.isBeta) BetaPill(),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
target == Target.myWhooshLink && Platform.isAndroid
|
||||
? 'Control MyWhoosh directly on this or another device'
|
||||
: target.isCompatible
|
||||
? target.description
|
||||
: 'Due to iOS restrictions only controlling trainer apps on other devices is supported.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
if (target == Target.myWhooshLink)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Divider(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -240,30 +272,23 @@ class LinkRequirement extends PlatformRequirement {
|
||||
|
||||
@override
|
||||
Widget? build(BuildContext context, VoidCallback onUpdate) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: whooshLink.isStarted,
|
||||
builder: (BuildContext context, value, Widget? child) {
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: value
|
||||
? null
|
||||
: () async {
|
||||
await whooshLink.startServer();
|
||||
onUpdate();
|
||||
},
|
||||
child: Text(value ? 'Waiting for MyWhoosh...' : 'Start MyWhoosh Link Server'),
|
||||
),
|
||||
if (value) SmallProgressIndicator(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
return LinkWidget(onUpdate: onUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> getStatus() async {
|
||||
status = whooshLink.isConnected.value || kDebugMode;
|
||||
status = whooshLink.isConnected.value;
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceholderRequirement extends PlatformRequirement {
|
||||
PlaceholderRequirement() : super('Requirement');
|
||||
|
||||
@override
|
||||
Future<void> call(BuildContext context, VoidCallback onUpdate) async {}
|
||||
|
||||
@override
|
||||
Future<void> getStatus() async {
|
||||
status = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ Future<List<PlatformRequirement>> getRequirements(ConnectionType connectionType)
|
||||
ConnectionType.local => KeyboardRequirement(),
|
||||
ConnectionType.remote => RemoteRequirement(),
|
||||
ConnectionType.link => LinkRequirement(),
|
||||
ConnectionType.unknown => PlaceholderRequirement(),
|
||||
},
|
||||
BluetoothScanning(),
|
||||
];
|
||||
@@ -53,7 +54,12 @@ Future<List<PlatformRequirement>> getRequirements(ConnectionType connectionType)
|
||||
list = [
|
||||
TargetRequirement(),
|
||||
BluetoothTurnedOn(),
|
||||
RemoteRequirement(),
|
||||
switch (connectionType) {
|
||||
ConnectionType.local => RemoteRequirement(),
|
||||
ConnectionType.remote => RemoteRequirement(),
|
||||
ConnectionType.link => LinkRequirement(),
|
||||
ConnectionType.unknown => PlaceholderRequirement(),
|
||||
},
|
||||
BluetoothScanning(),
|
||||
];
|
||||
} else if (Platform.isWindows) {
|
||||
@@ -64,6 +70,7 @@ Future<List<PlatformRequirement>> getRequirements(ConnectionType connectionType)
|
||||
ConnectionType.local => KeyboardRequirement(),
|
||||
ConnectionType.remote => RemoteRequirement(),
|
||||
ConnectionType.link => LinkRequirement(),
|
||||
ConnectionType.unknown => PlaceholderRequirement(),
|
||||
},
|
||||
BluetoothScanning(),
|
||||
];
|
||||
@@ -84,6 +91,7 @@ Future<List<PlatformRequirement>> getRequirements(ConnectionType connectionType)
|
||||
ConnectionType.local => AccessibilityRequirement(),
|
||||
ConnectionType.remote => RemoteRequirement(),
|
||||
ConnectionType.link => LinkRequirement(),
|
||||
ConnectionType.unknown => PlaceholderRequirement(),
|
||||
},
|
||||
BluetoothScanning(),
|
||||
];
|
||||
|
||||
@@ -17,7 +17,7 @@ class Settings {
|
||||
|
||||
Future<void> init() async {
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
initializeActions(settings.getLastTarget()?.connectionType ?? ConnectionType.local);
|
||||
initializeActions(getLastTarget()?.connectionType ?? ConnectionType.unknown);
|
||||
|
||||
if (actionHandler is DesktopActions) {
|
||||
// Must add this line.
|
||||
@@ -215,4 +215,14 @@ class Settings {
|
||||
if (actionName == null) return button.action;
|
||||
return InGameAction.values.firstOrNullWhere((e) => e.name == actionName) ?? button.action;
|
||||
}
|
||||
|
||||
void setInGameActionForButtonValue(ControllerButton button, InGameAction inGameAction, int value) {
|
||||
final key = 'ingameaction_${button.name}_value';
|
||||
prefs.setInt(key, value);
|
||||
}
|
||||
|
||||
int? getInGameActionForButtonValue(ControllerButton button) {
|
||||
final key = 'ingameaction_${button.name}_value';
|
||||
return prefs.getInt(key);
|
||||
}
|
||||
}
|
||||
|
||||
27
lib/widgets/beta_pill.dart
Normal file
27
lib/widgets/beta_pill.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BetaPill extends StatelessWidget {
|
||||
const BetaPill({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'BETA',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/link/link.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/pages/device.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
|
||||
class IngameactionsCustomizer extends StatefulWidget {
|
||||
const IngameactionsCustomizer({super.key});
|
||||
class InGameActionsCustomizer extends StatefulWidget {
|
||||
const InGameActionsCustomizer({super.key});
|
||||
|
||||
@override
|
||||
State<IngameactionsCustomizer> createState() => _IngameactionsCustomizerState();
|
||||
State<InGameActionsCustomizer> createState() => _InGameActionsCustomizerState();
|
||||
}
|
||||
|
||||
class _IngameactionsCustomizerState extends State<IngameactionsCustomizer> {
|
||||
class _InGameActionsCustomizerState extends State<InGameActionsCustomizer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connectedDevice = connection.devices.firstOrNull;
|
||||
@@ -58,7 +59,9 @@ class _IngameactionsCustomizerState extends State<IngameactionsCustomizer> {
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Row(
|
||||
children: [
|
||||
IntrinsicWidth(child: ButtonWidget(button: button)),
|
||||
IntrinsicWidth(
|
||||
child: ButtonWidget(button: button),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -66,25 +69,10 @@ class _IngameactionsCustomizerState extends State<IngameactionsCustomizer> {
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Row(
|
||||
children: [
|
||||
DropdownButton<InGameAction>(
|
||||
isDense: true,
|
||||
items: InGameAction.values
|
||||
.map(
|
||||
(ingame) => DropdownMenuItem(
|
||||
value: ingame,
|
||||
child: Text(ingame.toString()),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: settings.getInGameActionForButton(button),
|
||||
onChanged: (action) {
|
||||
settings.setInGameActionForButton(
|
||||
button,
|
||||
action!,
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (MediaQuery.sizeOf(context).width < 1800)
|
||||
Expanded(child: _buildDropdownButton(button, true))
|
||||
else
|
||||
_buildDropdownButton(button, false),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -96,4 +84,52 @@ class _IngameactionsCustomizerState extends State<IngameactionsCustomizer> {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDropdownButton(ControllerButton button, bool expand) {
|
||||
final value = WhooshLink.supportedActions.contains(settings.getInGameActionForButton(button))
|
||||
? settings.getInGameActionForButton(button)
|
||||
: null;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButton<InGameAction>(
|
||||
isExpanded: expand,
|
||||
items: WhooshLink.supportedActions
|
||||
.map(
|
||||
(ingame) => DropdownMenuItem(
|
||||
value: ingame,
|
||||
child: Text(ingame.toString()),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
padding: EdgeInsets.zero,
|
||||
menuWidth: 250,
|
||||
value: value,
|
||||
onChanged: (action) {
|
||||
settings.setInGameActionForButton(
|
||||
button,
|
||||
action!,
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (value?.possibleValues != null)
|
||||
DropdownButton<int>(
|
||||
items: value!.possibleValues!
|
||||
.map((val) => DropdownMenuItem<int>(value: val, child: Text(val.toString())))
|
||||
.toList(),
|
||||
value: settings.getInGameActionForButtonValue(button),
|
||||
onChanged: (val) {
|
||||
settings.setInGameActionForButtonValue(
|
||||
button,
|
||||
value,
|
||||
val!,
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
hint: Text('Value'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
62
lib/widgets/link.dart
Normal file
62
lib/widgets/link.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/pages/markdown.dart';
|
||||
import 'package:swift_control/widgets/small_progress_indicator.dart';
|
||||
|
||||
class LinkWidget extends StatefulWidget {
|
||||
final VoidCallback onUpdate;
|
||||
const LinkWidget({super.key, required this.onUpdate});
|
||||
|
||||
@override
|
||||
State<LinkWidget> createState() => _LinkWidgetState();
|
||||
}
|
||||
|
||||
class _LinkWidgetState extends State<LinkWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
whooshLink.startServer();
|
||||
whooshLink.isConnected.addListener(() {
|
||||
widget.onUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: whooshLink.isStarted,
|
||||
builder: (BuildContext context, value, Widget? child) {
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: value
|
||||
? null
|
||||
: () async {
|
||||
await whooshLink.startServer();
|
||||
},
|
||||
child: Text(value ? 'Waiting for MyWhoosh...' : 'Start Listening for MyWhoosh'),
|
||||
),
|
||||
if (value) SmallProgressIndicator(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('Verify with the MyWhoosh Link app if connection is possible.'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')),
|
||||
);
|
||||
},
|
||||
child: const Text("Show Troubleshooting Guide"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,22 @@ List<Widget> buildMenuButtons() {
|
||||
PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text('Instructions'),
|
||||
onTap: () {
|
||||
final instructions = Platform.isAndroid
|
||||
? 'INSTRUCTIONS_ANDROID.md'
|
||||
: Platform.isIOS
|
||||
? 'INSTRUCTIONS_IOS.md'
|
||||
: Platform.isMacOS
|
||||
? 'INSTRUCTIONS_MACOS.md'
|
||||
: 'INSTRUCTIONS_WINDOWS.md';
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: instructions)),
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text('Troubleshooting Guide'),
|
||||
onTap: () {
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/pages/markdown.dart';
|
||||
import 'package:swift_control/widgets/small_progress_indicator.dart';
|
||||
|
||||
import '../widgets/logviewer.dart';
|
||||
import 'logviewer.dart';
|
||||
|
||||
class ScanWidget extends StatefulWidget {
|
||||
const ScanWidget({super.key});
|
||||
@@ -58,6 +58,10 @@ flutter:
|
||||
assets:
|
||||
- CHANGELOG.md
|
||||
- TROUBLESHOOTING.md
|
||||
- INSTRUCTIONS_ANDROID.md
|
||||
- INSTRUCTIONS_IOS.md
|
||||
- INSTRUCTIONS_WINDOWS.md
|
||||
- INSTRUCTIONS_MACOS.md
|
||||
- shorebird.yaml
|
||||
|
||||
msix_config:
|
||||
|
||||
Reference in New Issue
Block a user