cleanup, fixes

This commit is contained in:
Jonas Bark
2025-10-27 13:43:18 +01:00
parent 828aa70a56
commit bb1ae4e616
12 changed files with 229 additions and 170 deletions

View File

@@ -160,9 +160,6 @@ class Connection {
if (existing != null) {
existing.isConnected = true;
signalChange(existing);
} else {
final linkDevice = LinkDevice(socket.remoteAddress.address);
_addDevices([linkDevice]);
}
},
onDisconnected: (socket) {

View File

@@ -27,30 +27,42 @@ class LinkDevice extends BaseDevice {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('MyWhoosh Link: ${isConnected ? 'Connected' : 'Not connected'}'),
if (isConnected)
PopupMenuButton(
itemBuilder: (c) => [
PopupMenuItem(
child: Text('Disconnect'),
onTap: () {
connection.disconnect(this, forget: true);
},
),
],
)
else
LoadingWidget(
futureCallback: () => connection.startMyWhooshServer(),
renderChild: (isLoading, tap) => ValueListenableBuilder(
valueListenable: whooshLink.isConnected,
builder: (c, isConnected, _) => TextButton(
onPressed: !isConnected ? tap : null,
child: isLoading || (!isConnected && whooshLink.isStarted.value)
? SmallProgressIndicator()
: Text('Connect'),
Row(
children: [
if (!isConnected)
LoadingWidget(
futureCallback: () => connection.startMyWhooshServer(),
renderChild: (isLoading, tap) => ValueListenableBuilder(
valueListenable: whooshLink.isConnected,
builder: (c, isConnected, _) => TextButton(
onPressed: !isConnected ? tap : null,
child: isLoading || (!isConnected && whooshLink.isStarted.value)
? SmallProgressIndicator()
: Text('Connect'),
),
),
),
PopupMenuButton(
itemBuilder: (c) => [
if (isConnected)
PopupMenuItem(
child: Text('Disconnect'),
onTap: () {
connection.disconnect(this, forget: true);
},
)
else
PopupMenuItem(
child: Text('Stop'),
onTap: () {
whooshLink.stopServer();
},
),
],
),
),
],
),
],
);
}

View File

@@ -1,8 +1,10 @@
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart';
import 'package:swift_control/pages/markdown.dart';
import 'package:swift_control/widgets/warning.dart';
class ZwiftClickV2 extends ZwiftRide {
ZwiftClickV2(super.scanResult) : super(isBeta: true);
@@ -19,6 +21,58 @@ class ZwiftClickV2 extends ZwiftRide {
await sendCommandBuffer(Uint8List.fromList([0xFF, 0x04, 0x00]));
}
@override
Widget showInformation(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
super.showInformation(context),
if (isConnected)
Warning(
children: [
Text(
'''To make your Zwift Click V2 work best you should connect it in the Zwift app once each day.\nIf you don't do that SwiftControl will need to reconnect every minute.
1. Open Zwift app
2. Log in (subscription not required) and open the device connection screen
3. Connect your Trainer, then connect the Zwift Click V2
4. Close the Zwift app again and connect again in SwiftControl''',
),
Row(
children: [
TextButton(
onPressed: () {
sendCommand(Opcode.RESET, null);
},
child: Text('Reset now'),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md'),
),
);
},
child: Text('Troubleshooting'),
),
if (kDebugMode)
TextButton(
onPressed: () {
test();
},
child: Text('Test'),
),
],
),
],
),
],
);
}
Future<void> test() async {
await sendCommand(Opcode.RESET, null);
//await sendCommand(Opcode.GET, Get(dataObjectId: VendorDO.PAGE_DEVICE_PAIRING.value)); // 0008 82E0 03

View File

@@ -5,15 +5,14 @@ import 'package:device_auto_rotate_checker/device_auto_rotate_checker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_clickv2.dart';
import 'package:swift_control/bluetooth/devices/link/link_device.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/pages/markdown.dart';
import 'package:swift_control/utils/actions/desktop.dart';
import 'package:swift_control/utils/keymap/manager.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
import 'package:swift_control/widgets/logviewer.dart';
import 'package:swift_control/widgets/scan.dart';
import 'package:swift_control/widgets/small_progress_indicator.dart';
import 'package:swift_control/widgets/testbed.dart';
import 'package:swift_control/widgets/title.dart';
import 'package:swift_control/widgets/warning.dart';
@@ -54,12 +53,16 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
_checkAndShowChangelog();
});
if (actionHandler is RemoteActions && !kIsWeb && Platform.isIOS) {
whooshLink.isStarted.addListener(() {
if (mounted) setState(() {});
});
if (actionHandler is RemoteActions && !kIsWeb && Platform.isIOS && (actionHandler as RemoteActions).isConnected) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// show snackbar to inform user that the app needs to stay in foreground
_snackBarMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text('To keep working properly the app needs to stay in the foreground.'),
content: Text('To simulate touches the app needs to stay in the foreground.'),
duration: Duration(seconds: 5),
),
);
@@ -98,14 +101,14 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
if (actionHandler is RemoteActions && Platform.isIOS) {
if (actionHandler is RemoteActions && Platform.isIOS && (actionHandler as RemoteActions).isConnected) {
UniversalBle.getBluetoothAvailabilityState().then((state) {
if (state == AvailabilityState.poweredOn) {
final requirement = RemoteRequirement();
requirement.reconnect();
_snackBarMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text('To keep working properly the app needs to stay in the foreground.'),
content: Text('To simulate touches the app needs to stay in the foreground.'),
duration: Duration(seconds: 5),
),
);
@@ -191,32 +194,40 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (connection.controllerDevices.isEmpty)
ScanWidget()
else
Container(
margin: const EdgeInsets.only(bottom: 8.0),
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.primaryContainer,
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
'Connected Controllers',
style: TextStyle(fontWeight: FontWeight.bold),
Container(
margin: const EdgeInsets.only(bottom: 8.0),
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.primaryContainer,
),
),
),
...connection.controllerDevices.map(
(device) => device.showInformation(context),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Connected Controllers',
style: TextStyle(fontWeight: FontWeight.bold),
),
if (connection.controllerDevices.isEmpty) SmallProgressIndicator(),
],
),
),
),
if (connection.controllerDevices.isEmpty)
ScanWidget()
else
...connection.controllerDevices.map(
(device) => device.showInformation(context),
),
if (connection.remoteDevices.isNotEmpty || actionHandler is RemoteActions)
if (connection.remoteDevices.isNotEmpty ||
actionHandler is RemoteActions ||
whooshLink.isStarted.value)
Container(
margin: const EdgeInsets.only(bottom: 8.0),
width: double.infinity,
@@ -230,7 +241,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
'Remote Devices',
'Remote Connections',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
@@ -239,6 +250,8 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
(device) => device.showInformation(context),
),
if (whooshLink.isStarted.value) LinkDevice('').showInformation(context),
if (actionHandler is RemoteActions)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -259,53 +272,6 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
),
],
),
if (connection.devices.any((device) => (device is ZwiftClickV2) && device.isConnected))
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Warning(
children: [
Text(
'''To make your Zwift Click V2 work best you should connect it in the Zwift app once each day.\nIf you don't do that SwiftControl will need to reconnect every minute.
1. Open Zwift app
2. Log in (subscription not required) and open the device connection screen
3. Connect your Trainer, then connect the Zwift Click V2
4. Close the Zwift app again and connect again in SwiftControl''',
),
Row(
children: [
TextButton(
onPressed: () {
connection.devices.whereType<ZwiftClickV2>().forEach(
(device) => device.sendCommand(Opcode.RESET, null),
);
},
child: Text('Reset now'),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md'),
),
);
},
child: Text('Troubleshooting'),
),
if (kDebugMode)
TextButton(
onPressed: () {
(connection.bluetoothDevices.first as ZwiftClickV2).test();
},
child: Text('Test'),
),
],
),
],
),
),
],
),
),

View File

@@ -84,7 +84,9 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
if (_requirements[step].status && _requirements[step] is! TargetRequirement) {
return;
}
final hasEarlierIncomplete = _requirements.indexWhere((req) => !req.status) < step;
final hasEarlierIncomplete =
_requirements.indexWhere((req) => !req.status) != -1 &&
_requirements.indexWhere((req) => !req.status) < step;
if (hasEarlierIncomplete) {
return;
}

View File

@@ -53,7 +53,7 @@ class AndroidActions extends BaseActions {
try {
await accessibilityHandler.performTouch(point.dx, point.dy, isKeyDown: isKeyDown, isKeyUp: isKeyUp);
} on PlatformException catch (e) {
return "Failed to perform touch action. Please get in contact with Jonas.\n${e.message}";
return "Accessibility Service not working. Follow instructions at https://dontkillmyapp.com/";
}
return "Touch performed at: ${point.dx.toInt()}, ${point.dy.toInt()} -> ${isKeyDown && isKeyUp
? "click"

View File

@@ -30,7 +30,7 @@ class RemoteActions extends BaseActions {
if (keyPair.inGameAction != null && whooshLink.isConnected.value) {
return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
} else if (!(actionHandler as RemoteActions).isConnected) {
return 'Not connected to a device';
return 'Not connected to a ${settings.getLastTarget()?.name ?? 'remote'} device';
}
if (keyPair.physicalKey != null && keyPair.touchPosition == Offset.zero) {

View File

@@ -89,25 +89,24 @@ enum Target {
),
iPad(
title: 'iPad',
description: 'Remotely control any trainer app on an iPad by acting as a Mouse',
description: 'Remotely control any trainer app on an iPad by acting as a Mouse, or directly via MyWhoosh Link',
icon: Icons.settings_remote_outlined,
isBeta: true,
),
android(
title: 'Android Device',
description: 'Remotely control any trainer app on an Android device',
description: 'Remotely control any trainer app on another Android device, or directly via MyWhoosh Link',
icon: Icons.settings_remote_outlined,
isBeta: true,
),
macOS(
title: 'Mac',
description: 'Remotely control any trainer app on a Mac',
description: 'Remotely control any trainer app on another Mac, or directly via MyWhoosh Link',
icon: Icons.settings_remote_outlined,
isBeta: true,
),
windows(
title: 'Windows PC',
description: 'Remotely control any trainer app on a Windows PC',
description: 'Remotely control any trainer app on another Windows PC, or directly via MyWhoosh Link',
icon: Icons.settings_remote_outlined,
isBeta: true,
);
@@ -183,7 +182,7 @@ class TargetRequirement extends PlatformRequirement {
Row(
children: [
Text(target.title, style: TextStyle(fontWeight: FontWeight.bold)),
if (target.isBeta) BetaPill(),
if (target.isBeta || (!Platform.isIOS && target == Target.iPad)) BetaPill(),
],
),
Text(

View File

@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide ConnectionState;
import 'package:permission_handler/permission_handler.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/pages/device.dart';
import 'package:swift_control/utils/actions/remote.dart';
import 'package:swift_control/utils/requirements/multi.dart';
import 'package:swift_control/utils/requirements/platform.dart';
@@ -92,6 +93,11 @@ class RemoteRequirement extends PlatformRequirement {
return;
}
}
if (kDebugMode) {
print('Continuing');
return;
}
while (peripheralManager.state != BluetoothLowEnergyState.poweredOn) {
print('Waiting for peripheral manager to be powered on...');
if (settings.getLastTarget() == Target.thisDevice) {
@@ -99,7 +105,6 @@ class RemoteRequirement extends PlatformRequirement {
}
await Future.delayed(Duration(seconds: 1));
}
if (!_isServiceAdded) {
await Future.delayed(Duration(seconds: 1));
final reportMapDataAbsolute = Uint8List.fromList([
@@ -318,6 +323,30 @@ class _PairWidgetState extends State<_PairWidget> {
if (_isAdvertising || _isLoading) SizedBox(height: 20, width: 20, child: SmallProgressIndicator()),
],
),
ElevatedButton(
onPressed: () async {
Navigator.push(
context,
MaterialPageRoute(
builder: (c) => DevicePage(),
settings: RouteSettings(name: '/device'),
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Use MyWhoosh Link only'),
Text(
'No pairing required, connect directly via MyWhoosh Link.',
style: TextStyle(fontSize: 10, color: Colors.black87),
),
],
),
),
),
if (_isAdvertising) ...[
TextButton(
onPressed: () {

View File

@@ -36,7 +36,7 @@ class ChangelogDialog extends StatelessWidget {
static Future<void> showIfNeeded(BuildContext context, String currentVersion, String? lastSeenVersion) async {
// Show dialog if this is a new version
if (lastSeenVersion != currentVersion || true) {
if (lastSeenVersion != currentVersion) {
try {
final entry = await rootBundle.loadString('CHANGELOG.md');
if (context.mounted) {

View File

@@ -94,27 +94,33 @@ class _KeymapExplanationState extends State<KeymapExplanation> {
for (final keyPair in availableKeypairs) ...[
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (actionHandler.supportedApp is! CustomApp)
if (keyPair.buttons.filter((b) => allAvailableButtons.contains(b)).isEmpty)
Text('No button assigned for your connected device')
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Container(
padding: const EdgeInsets.all(6),
child: Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (actionHandler.supportedApp is! CustomApp)
if (keyPair.buttons.filter((b) => allAvailableButtons.contains(b)).isEmpty)
Text('No button assigned for your connected device')
else
for (final button in keyPair.buttons.filter((b) => allAvailableButtons.contains(b)))
IntrinsicWidth(child: ButtonWidget(button: button))
else
for (final button in keyPair.buttons.filter((b) => allAvailableButtons.contains(b)))
IntrinsicWidth(child: ButtonWidget(button: button))
else
for (final button in keyPair.buttons) IntrinsicWidth(child: ButtonWidget(button: button)),
],
for (final button in keyPair.buttons) IntrinsicWidth(child: ButtonWidget(button: button)),
],
),
),
),
Padding(
padding: const EdgeInsets.all(6),
child: _ButtonEditor(keyPair: keyPair, onUpdate: widget.onUpdate),
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(6),
child: _ButtonEditor(keyPair: keyPair, onUpdate: widget.onUpdate),
),
),
],
),
@@ -296,43 +302,39 @@ class _ButtonEditor extends StatelessWidget {
),
];
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (keyPair.buttons.isNotEmpty &&
(keyPair.physicalKey != null || keyPair.touchPosition != Offset.zero || keyPair.inGameAction != null))
Expanded(
child: KeypairExplanation(
keyPair: keyPair,
),
)
else
Expanded(child: Text('No action assigned')),
return Container(
constraints: BoxConstraints(minHeight: kMinInteractiveDimension - 6),
padding: EdgeInsets.only(right: actionHandler.supportedApp is CustomApp ? 4 : 0),
child: PopupMenuButton(
itemBuilder: (c) => actions,
enabled: actionHandler.supportedApp is CustomApp,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (keyPair.buttons.isNotEmpty &&
(keyPair.physicalKey != null || keyPair.touchPosition != Offset.zero || keyPair.inGameAction != null))
Expanded(
child: KeypairExplanation(
keyPair: keyPair,
),
)
else
Expanded(child: Text('No action assigned')),
if (actionHandler.supportedApp is CustomApp)
PopupMenuButton<PhysicalKeyboardKey>(
enabled: true,
itemBuilder: (context) => [
if (actions.length > 1) ...actions,
],
onSelected: (key) {
keyPair.physicalKey = key;
keyPair.logicalKey = null;
onUpdate();
},
icon: Icon(Icons.edit),
)
else
IconButton(
onPressed: () async {
final currentProfile = actionHandler.supportedApp!.name;
await KeymapManager().duplicate(context, currentProfile);
onUpdate();
},
icon: Icon(Icons.edit),
),
],
if (actionHandler.supportedApp is! CustomApp)
IconButton(
onPressed: () async {
final currentProfile = actionHandler.supportedApp!.name;
await KeymapManager().duplicate(context, currentProfile);
onUpdate();
},
icon: Icon(Icons.edit),
)
else
Icon(Icons.edit, size: 14),
],
),
),
);
}
}

View File

@@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart';
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 ScanWidget extends StatefulWidget {
const ScanWidget({super.key});
@@ -67,7 +66,6 @@ class _ScanWidgetState extends State<ScanWidget> {
},
child: const Text("Show Troubleshooting Guide"),
),
SmallProgressIndicator(),
SizedBox(),
],
);