bugfixes and clarifications

This commit is contained in:
Jonas Bark
2025-12-20 10:01:37 +01:00
parent 39b49bb9de
commit fac2e86240
19 changed files with 168 additions and 127 deletions

View File

@@ -52,8 +52,8 @@ Best follow our landing page and the "Get Started" button: [bikecontrol.app](htt
- CYCPLUS BC2 Virtual Shifter
- Elite Sterzo Smart (for steering support)
- Elite Square Smart Frame (beta)
- Gyroscope/Accelerometer Steering (for mobile devices)
- Mount your phone on the handlebar for steering detection
- Your Phone!
- Mount your phone on the handlebar to detect e.g. steering
- Available on Android and iOS
- Gamepads
- Keyboard input

View File

@@ -52,25 +52,25 @@ abstract class BaseDevice {
Future<void> connect();
Future<void> handleButtonsClickedWithoutLongPressSupport(List<ControllerButton> clickedButtons) async {
await handleButtonsClicked(clickedButtons);
await handleButtonsClicked(clickedButtons, longPress: true);
if (clickedButtons.length == 1) {
final keyPair = core.actionHandler.supportedApp?.keymap.getKeyPair(clickedButtons.single);
if (keyPair != null && (keyPair.isLongPress || keyPair.inGameAction?.isLongPress == true)) {
// simulate release after click
_longPressTimer?.cancel();
await Future.delayed(const Duration(milliseconds: 800));
await handleButtonsClicked([]);
await handleButtonsClicked([], longPress: true);
} else {
await handleButtonsClicked([]);
await handleButtonsClicked([], longPress: true);
}
} else {
await handleButtonsClicked([]);
}
}
Future<void> handleButtonsClicked(List<ControllerButton>? buttonsClicked) async {
Future<void> handleButtonsClicked(List<ControllerButton>? buttonsClicked, {bool longPress = false}) async {
try {
await _handleButtonsClickedInternal(buttonsClicked);
await _handleButtonsClickedInternal(buttonsClicked, longPress: longPress);
} catch (e, st) {
actionStreamInternal.add(
LogNotification('Error handling button clicks: $e\n$st'),
@@ -78,7 +78,7 @@ abstract class BaseDevice {
}
}
Future<void> _handleButtonsClickedInternal(List<ControllerButton>? buttonsClicked) async {
Future<void> _handleButtonsClickedInternal(List<ControllerButton>? buttonsClicked, {required bool longPress}) async {
if (buttonsClicked == null) {
// ignore, no changes
} else if (buttonsClicked.isEmpty) {
@@ -88,8 +88,9 @@ abstract class BaseDevice {
// Handle release events for long press keys
final buttonsReleased = _previouslyPressedButtons.toList();
final isLongPress =
longPress ||
buttonsReleased.singleOrNull != null &&
core.actionHandler.supportedApp?.keymap.getKeyPair(buttonsReleased.single)?.isLongPress == true;
core.actionHandler.supportedApp?.keymap.getKeyPair(buttonsReleased.single)?.isLongPress == true;
if (buttonsReleased.isNotEmpty && isLongPress) {
await performRelease(buttonsReleased);
}
@@ -100,15 +101,17 @@ abstract class BaseDevice {
// Handle release events for buttons that are no longer pressed
final buttonsReleased = _previouslyPressedButtons.difference(buttonsClicked.toSet()).toList();
final wasLongPress =
longPress ||
buttonsReleased.singleOrNull != null &&
core.actionHandler.supportedApp?.keymap.getKeyPair(buttonsReleased.single)?.isLongPress == true;
core.actionHandler.supportedApp?.keymap.getKeyPair(buttonsReleased.single)?.isLongPress == true;
if (buttonsReleased.isNotEmpty && wasLongPress) {
await performRelease(buttonsReleased);
}
final isLongPress =
longPress ||
buttonsClicked.singleOrNull != null &&
core.actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true;
core.actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true;
if (!isLongPress &&
!(buttonsClicked.singleOrNull == ZwiftButtons.onOffLeft ||
@@ -201,6 +204,9 @@ abstract class BaseDevice {
Widget showInformation(BuildContext context);
ControllerButton getOrAddButton(String key, ControllerButton Function() creator) {
if (core.actionHandler.supportedApp == null) {
return creator();
}
if (core.actionHandler.supportedApp is! CustomApp) {
final currentProfile = core.actionHandler.supportedApp!.name;
// should we display this to the user?

View File

@@ -53,8 +53,7 @@ class SramAxs extends BluetoothDevice {
void _emitClick(ControllerButton button) {
// Use the common pipeline so long-press handling and app action execution stays consistent.
handleButtonsClicked([button]);
handleButtonsClicked([]);
handleButtonsClickedWithoutLongPressSupport([button]);
}
void _registerTap() {
@@ -117,10 +116,10 @@ class SramAxs extends BluetoothDevice {
Text(
"Unfortunately, at the moment it's not possible to determine which physical button was pressed on your SRAM AXS device. Let us know if you have a contact at SRAM who can help :)\n\n"
'So the app exposes two logical buttons:\n'
'• SRAM Action (Single Click), assigned to Shift Up\n'
'• SRAM Action (Double Click), assigned to Shift Down\n\n'
'• SRAM Tap, assigned to Shift Up\n'
'• SRAM Double Tap, assigned to Shift Down\n\n'
'You can assign an action to each in the app settings.',
).small,
).xSmall,
Builder(
builder: (context) {
return PrimaryButton(

View File

@@ -380,7 +380,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
_write(_socket!, zero);
}
if (kDebugMode) {
print('Sent action ${keyPair.inGameAction!.title} to Zwift Emulator');
print('Sent action $isKeyUp vs $isKeyDown ${keyPair.inGameAction!.title} to Zwift Emulator');
}
return Success('Sent action: ${keyPair.inGameAction!.title}');
}

View File

@@ -1,7 +1,5 @@
import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:bike_control/bluetooth/devices/bluetooth_device.dart';
import 'package:bike_control/bluetooth/devices/zwift/constants.dart';
import 'package:bike_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart';
@@ -9,6 +7,8 @@ import 'package:bike_control/bluetooth/messages/notification.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/keymap/buttons.dart';
import 'package:bike_control/utils/single_line_exception.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:universal_ble/universal_ble.dart';
abstract class ZwiftDevice extends BluetoothDevice {
@@ -152,10 +152,10 @@ abstract class ZwiftDevice extends BluetoothDevice {
}
@override
Future<void> handleButtonsClicked(List<ControllerButton>? buttonsClicked) async {
Future<void> handleButtonsClicked(List<ControllerButton>? buttonsClicked, {bool longPress = false}) async {
// the same messages are sent multiple times, so ignore
if (_lastButtonsClicked == null || _lastButtonsClicked?.contentEquals(buttonsClicked ?? []) == false) {
super.handleButtonsClicked(buttonsClicked);
super.handleButtonsClicked(buttonsClicked, longPress: longPress);
}
_lastButtonsClicked = buttonsClicked;
}

View File

@@ -144,6 +144,7 @@
"enableMediaKeyDetection": "Medientastenerkennung aktivieren",
"enablePairingProcess": "Kopplungsprozess aktivieren",
"enablePermissions": "Berechtigungen aktivieren",
"enableSteeringWithPhone": "Sensoren Ihres Telefons aktivieren z.B. zum Lenken",
"enableVibrationFeedback": "Vibrationsfeedback beim Gangwechsel aktivieren",
"enableZwiftControllerBluetooth": "Zwift Controller aktivieren (Bluetooth)",
"enableZwiftControllerNetwork": "Zwift Controller aktivieren (Netzwerk)",

View File

@@ -144,6 +144,7 @@
"enableMediaKeyDetection": "Enable Media Key Detection",
"enablePairingProcess": "Enable Pairing Process",
"enablePermissions": "Enable Permissions",
"enableSteeringWithPhone": "Enable Phones' sensors to enable e.g. steering",
"enableVibrationFeedback": "Enable vibration feedback when shifting gears",
"enableZwiftControllerBluetooth": "Enable Zwift Controller (Bluetooth)",
"enableZwiftControllerNetwork": "Enable Zwift Controller (Network)",
@@ -421,6 +422,5 @@
"whatsNew": "What's New",
"whyPermissionNeeded": "Why is this permission needed?",
"zwiftControllerAction": "Zwift Controller Action",
"zwiftControllerDescription": "Enables BikeControl to act as a Zwift-compatible controller.",
"enableSteeringWithPhone": "Enable Steering using your phone's sensors"
}
"zwiftControllerDescription": "Enables BikeControl to act as a Zwift-compatible controller."
}

View File

@@ -144,6 +144,7 @@
"enableMediaKeyDetection": "Activer la détection des touches multimédias",
"enablePairingProcess": "Activer le processus d'appairage",
"enablePermissions": "Activer les autorisations",
"enableSteeringWithPhone": "Activez les capteurs du téléphone pour permettre, par exemple, la direction.",
"enableVibrationFeedback": "Activer le retour haptique par vibration lors du changement de vitesse",
"enableZwiftControllerBluetooth": "Activer le contrôleur Zwift (Bluetooth)",
"enableZwiftControllerNetwork": "Activer le contrôleur Zwift (réseau)",

View File

@@ -144,6 +144,7 @@
"enableMediaKeyDetection": "Włącz rozpoznawanie klawiszy multimedialnych",
"enablePairingProcess": "Włącz proces parowania",
"enablePermissions": "Nadaj uprawnienia",
"enableSteeringWithPhone": "Włącz czujniki telefonów, aby umożliwić np. sterowanie",
"enableVibrationFeedback": "Włącz wibracje podczas zmiany biegów",
"enableZwiftControllerBluetooth": "Włącz kontroler Zwift (Bluetooth)",
"enableZwiftControllerNetwork": "Włącz kontroler Zwift (sieć)",
@@ -163,7 +164,7 @@
"firmware": "Firmware",
"forceCloseToUpdate": "Wymuś zamknięcie aplikacji, aby móc korzystać z nowej wersji",
"fullVersion": "Pełna wersja",
"fullVersionDescription": "The full version includes:\n- Unlimited commands per day\n- Access to all future updates\n- No subscription! A one-time fee only :)",
"fullVersionDescription": "Pełna wersja zawiera: \n- Nielimitowane polecenia dziennie \n- Dostęp do wszystkich przyszłych aktualizacji \n- Brak subskrypcji! Opłata jednorazowa :)",
"getSupport": "Uzyskaj wsparcie",
"gotIt": "Zrozumiałem!",
"grant": "Nadaj",

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:bike_control/gen/l10n.dart';
import 'package:bike_control/main.dart';
import 'package:bike_control/pages/button_simulator.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/i18n_extension.dart';
import 'package:bike_control/utils/iap/iap_manager.dart';
@@ -12,7 +11,6 @@ import 'package:bike_control/widgets/ui/colored_title.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import '../bluetooth/devices/base_device.dart';
import '../widgets/ignored_devices_dialog.dart';
class DevicePage extends StatefulWidget {
final VoidCallback onUpdate;
@@ -96,18 +94,6 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
),
],
if (core.settings.getIgnoredDevices().isNotEmpty)
OutlineButton(
child: Text(context.i18n.manageIgnoredDevices),
onPressed: () async {
await showDialog(
context: context,
builder: (context) => IgnoredDevicesDialog(),
);
setState(() {});
},
),
SizedBox(),
if (core.connection.controllerDevices.isNotEmpty)
Row(
@@ -121,18 +107,6 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
},
),
],
)
else
PrimaryButton(
child: Text(AppLocalizations.of(context).noControllerUseCompanionMode),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (c) => ButtonSimulator(),
),
);
},
),
],
),

View File

@@ -135,6 +135,7 @@ class _NavigationState extends State<Navigation> {
backgroundColor: Theme.of(context).colorScheme.background,
trailing: buildMenuButtons(
context,
_selectedPage,
_isMobile
? () {
setState(() {

View File

@@ -3,6 +3,7 @@ import 'package:bike_control/gen/l10n.dart';
import 'package:bike_control/main.dart';
import 'package:bike_control/pages/button_simulator.dart';
import 'package:bike_control/pages/configuration.dart';
import 'package:bike_control/pages/navigation.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/i18n_extension.dart';
import 'package:bike_control/utils/iap/iap_manager.dart';
@@ -97,7 +98,7 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
controller: _scrollController,
child: SingleChildScrollView(
controller: _scrollController,
padding: EdgeInsets.symmetric(vertical: 32, horizontal: 16),
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -152,6 +153,21 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
),
if (core.logic.showLocalControl && !showLocalAsOther) LocalTile(),
if (core.logic.showMyWhooshLink && !showWhooshLinkAsOther) MyWhooshLinkTile(),
Text.rich(
TextSpan(
children: [
TextSpan(text: '${context.i18n.needHelpClickHelp} '),
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Icon(Icons.help_outline),
),
),
TextSpan(text: ' ${context.i18n.needHelpDontHesitate}'),
],
),
).small.muted,
if (core.logic.showRemote || showLocalAsOther || showWhooshLinkAsOther) ...[
SizedBox(height: 16),
Accordion(
@@ -171,21 +187,6 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
],
SizedBox(height: 4),
Text.rich(
TextSpan(
children: [
TextSpan(text: '${context.i18n.needHelpClickHelp} '),
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Icon(Icons.help_outline),
),
),
TextSpan(text: ' ${context.i18n.needHelpDontHesitate}'),
],
),
).small.muted,
SizedBox(),
Flex(
direction: isMobile ? Axis.vertical : Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -193,6 +194,7 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
spacing: 8,
children: [
PrimaryButton(
leading: Icon(Icons.computer_outlined),
child: Text(
AppLocalizations.of(
context,
@@ -217,10 +219,11 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
},
),
PrimaryButton(
child: Text(context.i18n.adjustControllerButtons),
leading: Icon(BCPage.customization.icon),
onPressed: () {
widget.goToNextPage();
},
child: Text(context.i18n.adjustControllerButtons),
),
],
),

View File

@@ -160,8 +160,8 @@ class IAPManager {
_windowsIapService?.dispose();
}
void reset() {
void reset(bool fullReset) {
_windowsIapService?.reset();
_iapService?.reset();
_iapService?.reset(fullReset);
}
}

View File

@@ -62,6 +62,9 @@ class IAPService {
onDone: () => _subscription?.cancel(),
onError: (error) {
debugPrint('IAP Error: $error');
core.connection.signalNotification(
LogNotification('There was an error with in-app purchases: ${error.toString()}'),
);
// On error, default to allowing access
IAPManager.instance.isPurchased.value = false;
},
@@ -189,6 +192,9 @@ class IAPService {
}
final purchasedVersion = json['receipt']["original_application_version"];
core.connection.signalNotification(
LogNotification('Apple receipt validated for version: $purchasedVersion'),
);
IAPManager.instance.isPurchased.value = Version.parse(purchasedVersion) < Version(4, 2, 0);
if (IAPManager.instance.isPurchased.value) {
debugPrint('Apple receipt validation successful - granting full access');
@@ -218,8 +224,9 @@ class IAPService {
}
debugPrint('Existing Android user detected - granting full access');
}
} catch (e) {
} catch (e, s) {
debugPrint('Error checking Android previous purchase: $e');
recordError(e, s, context: 'Checking Android previous purchase');
}
}
@@ -240,6 +247,9 @@ class IAPService {
/// Handle purchase updates
Future<void> _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) async {
for (final purchase in purchaseDetailsList) {
core.connection.signalNotification(
LogNotification('Purchase found: ${purchase.productID} - ${purchase.status}'),
);
if (purchase.status == PurchaseStatus.purchased || purchase.status == PurchaseStatus.restored) {
IAPManager.instance.isPurchased.value = !kDebugMode;
await _prefs.write(key: _purchaseStatusKey, value: IAPManager.instance.isPurchased.value.toString());
@@ -376,7 +386,13 @@ class IAPService {
_subscription?.cancel();
}
void reset() {
_prefs.deleteAll();
void reset(bool fullReset) {
if (fullReset) {
_prefs.deleteAll();
} else {
_prefs.delete(key: _purchaseStatusKey);
_isInitialized = false;
initialize();
}
}
}

View File

@@ -69,8 +69,8 @@ class Settings {
Future<void> reset() async {
await prefs.clear();
IAPManager.instance.reset();
core.actionHandler.init(null);
IAPManager.instance.reset(true);
init();
}
void setTrainerApp(SupportedApp app) {

View File

@@ -40,18 +40,25 @@ class _IAPStatusWidgetState extends State<IAPStatusWidget> {
Widget build(BuildContext context) {
final iapManager = IAPManager.instance;
final isTrialExpired = iapManager.isTrialExpired;
if (isTrialExpired) {
_isSmall = false;
}
final trialDaysRemaining = iapManager.trialDaysRemaining;
final commandsRemaining = iapManager.commandsRemainingToday;
final dailyCommandCount = iapManager.dailyCommandCount;
return CardButton(
return Button(
onPressed: _isSmall
? () {
setState(() {
_isSmall = false;
});
}
: null,
: _handlePurchase,
style: ButtonStyle.card().withBackgroundColor(
color: Theme.of(context).colorScheme.muted,
hoverColor: Theme.of(context).colorScheme.primaryForeground,
),
child: AnimatedContainer(
duration: Duration(milliseconds: 700),
width: double.infinity,
@@ -127,6 +134,8 @@ class _IAPStatusWidgetState extends State<IAPStatusWidget> {
leadingAlignment: Alignment.centerLeft,
leading: Icon(Icons.lock),
title: Text(AppLocalizations.of(context).trialExpired(IAPManager.dailyCommandLimit)),
trailing: _isSmall ? Icon(Icons.expand_more) : null,
trailingAlignment: Alignment.centerRight,
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 6,

View File

@@ -1,13 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show SelectionArea;
import 'package:flutter/services.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/i18n_extension.dart';
import 'package:bike_control/widgets/ui/toast.dart';
import 'package:flutter/material.dart' show SelectionArea;
import 'package:flutter/services.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import '../bluetooth/messages/notification.dart';
@@ -112,23 +110,6 @@ class _LogviewerState extends State<LogViewer> {
),
),
),
if (!kIsWeb) ...[
Text(context.i18n.logsAreAlsoAt).muted.small,
CodeSnippet(
code: SelectableText(File('${Directory.current.path}/app.logs').path),
actions: [
IconButton(
icon: Icon(Icons.copy),
variance: ButtonVariance.outline,
onPressed: () {
Clipboard.setData(ClipboardData(text: File('${Directory.current.path}/app.logs').path));
buildToast(context, title: context.i18n.pathCopiedToClipboard);
},
),
],
),
],
],
),
);

View File

@@ -1,22 +1,25 @@
import 'dart:io';
import 'package:bike_control/bluetooth/devices/zwift/zwift_clickv2.dart';
import 'package:bike_control/gen/l10n.dart';
import 'package:bike_control/pages/markdown.dart';
import 'package:bike_control/pages/navigation.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/i18n_extension.dart';
import 'package:bike_control/widgets/title.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show showLicensePage;
import 'package:in_app_review/in_app_review.dart';
import 'package:intl/intl.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:bike_control/bluetooth/devices/zwift/zwift_clickv2.dart';
import 'package:bike_control/gen/l10n.dart';
import 'package:bike_control/pages/markdown.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/i18n_extension.dart';
import 'package:bike_control/widgets/title.dart';
import 'package:universal_ble/universal_ble.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
List<Widget> buildMenuButtons(BuildContext context, VoidCallback? openLogs) {
import '../utils/iap/iap_manager.dart';
List<Widget> buildMenuButtons(BuildContext context, BCPage currentPage, VoidCallback? openLogs) {
return [
Builder(
builder: (context) {
@@ -210,7 +213,7 @@ List<Widget> buildMenuButtons(BuildContext context, VoidCallback? openLogs) {
},
),
Gap(4),
BKMenuButton(openLogs: openLogs),
BKMenuButton(openLogs: openLogs, currentPage: currentPage),
];
}
@@ -231,7 +234,8 @@ ${core.connection.lastLogEntries.reversed.joinToString(separator: '\n', transfor
class BKMenuButton extends StatelessWidget {
final VoidCallback? openLogs;
const BKMenuButton({super.key, this.openLogs});
final BCPage currentPage;
const BKMenuButton({super.key, this.openLogs, required this.currentPage});
@override
Widget build(BuildContext context) {
@@ -267,6 +271,16 @@ class BKMenuButton extends StatelessWidget {
),
MenuDivider(),
],
if (currentPage == BCPage.logs) ...[
MenuButton(
child: Text('Reset IAP State'),
onPressed: (c) async {
IAPManager.instance.reset(false);
core.settings.init();
},
),
MenuDivider(),
],
if (openLogs != null)
MenuButton(
leading: Icon(Icons.article_outlined),

View File

@@ -1,10 +1,12 @@
import 'dart:io';
import 'package:bike_control/gen/l10n.dart';
import 'package:bike_control/pages/button_simulator.dart';
import 'package:bike_control/pages/markdown.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/i18n_extension.dart';
import 'package:bike_control/utils/requirements/platform.dart';
import 'package:bike_control/widgets/ignored_devices_dialog.dart';
import 'package:bike_control/widgets/ui/connection_method.dart';
import 'package:bike_control/widgets/ui/wifi_animation.dart';
import 'package:flutter/foundation.dart';
@@ -78,6 +80,7 @@ class _ScanWidgetState extends State<ScanWidget> {
),
],
),
SizedBox(),
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows))
ValueListenableBuilder(
valueListenable: core.mediaKeyHandler.isMediaKeyDetectionEnabled,
@@ -109,25 +112,57 @@ class _ScanWidgetState extends State<ScanWidget> {
},
),
SizedBox(),
if (core.connection.controllerDevices.isEmpty) ...[
OutlineButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')),
);
},
child: Text(context.i18n.showTroubleshootingGuide),
),
OutlineButton(
onPressed: () {
launchUrlString(
'https://github.com/jonasbark/swiftcontrol/?tab=readme-ov-file#supported-devices',
);
},
child: Text(context.i18n.showSupportedControllers),
),
],
Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
OutlineButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')),
);
},
leading: Icon(Icons.help_outline),
child: Text(context.i18n.showTroubleshootingGuide),
),
OutlineButton(
onPressed: () {
launchUrlString(
'https://github.com/jonasbark/swiftcontrol/?tab=readme-ov-file#supported-devices',
);
},
leading: Icon(Icons.gamepad_outlined),
child: Text(context.i18n.showSupportedControllers),
),
if (core.settings.getIgnoredDevices().isNotEmpty)
OutlineButton(
leading: Icon(Icons.block_outlined),
onPressed: () async {
await showDialog(
context: context,
builder: (context) => IgnoredDevicesDialog(),
);
setState(() {});
},
child: Text(context.i18n.manageIgnoredDevices),
),
if (core.connection.controllerDevices.isEmpty)
PrimaryButton(
leading: Icon(Icons.computer_outlined),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (c) => ButtonSimulator(),
),
);
},
child: Text(AppLocalizations.of(context).noControllerUseCompanionMode),
),
],
),
],
);
} else {