diff --git a/README.md b/README.md index 66b6248..6c79a23 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/bluetooth/devices/base_device.dart b/lib/bluetooth/devices/base_device.dart index 73a8d44..4b6cd7c 100644 --- a/lib/bluetooth/devices/base_device.dart +++ b/lib/bluetooth/devices/base_device.dart @@ -52,25 +52,25 @@ abstract class BaseDevice { Future connect(); Future handleButtonsClickedWithoutLongPressSupport(List 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 handleButtonsClicked(List? buttonsClicked) async { + Future handleButtonsClicked(List? 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 _handleButtonsClickedInternal(List? buttonsClicked) async { + Future _handleButtonsClickedInternal(List? 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? diff --git a/lib/bluetooth/devices/sram/sram_axs.dart b/lib/bluetooth/devices/sram/sram_axs.dart index f691767..df17f1d 100644 --- a/lib/bluetooth/devices/sram/sram_axs.dart +++ b/lib/bluetooth/devices/sram/sram_axs.dart @@ -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( diff --git a/lib/bluetooth/devices/zwift/ftms_mdns_emulator.dart b/lib/bluetooth/devices/zwift/ftms_mdns_emulator.dart index 50ccbe5..87be6f5 100644 --- a/lib/bluetooth/devices/zwift/ftms_mdns_emulator.dart +++ b/lib/bluetooth/devices/zwift/ftms_mdns_emulator.dart @@ -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}'); } diff --git a/lib/bluetooth/devices/zwift/zwift_device.dart b/lib/bluetooth/devices/zwift/zwift_device.dart index 459934e..d6ee80f 100644 --- a/lib/bluetooth/devices/zwift/zwift_device.dart +++ b/lib/bluetooth/devices/zwift/zwift_device.dart @@ -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 handleButtonsClicked(List? buttonsClicked) async { + Future handleButtonsClicked(List? 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; } diff --git a/lib/i10n/intl_de.arb b/lib/i10n/intl_de.arb index e2737bc..1ea6bec 100644 --- a/lib/i10n/intl_de.arb +++ b/lib/i10n/intl_de.arb @@ -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)", diff --git a/lib/i10n/intl_en.arb b/lib/i10n/intl_en.arb index b39c950..989f91e 100644 --- a/lib/i10n/intl_en.arb +++ b/lib/i10n/intl_en.arb @@ -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." +} \ No newline at end of file diff --git a/lib/i10n/intl_fr.arb b/lib/i10n/intl_fr.arb index f556157..c472eee 100644 --- a/lib/i10n/intl_fr.arb +++ b/lib/i10n/intl_fr.arb @@ -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)", diff --git a/lib/i10n/intl_pl.arb b/lib/i10n/intl_pl.arb index 3c0d19b..8a1ea64 100644 --- a/lib/i10n/intl_pl.arb +++ b/lib/i10n/intl_pl.arb @@ -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", diff --git a/lib/pages/device.dart b/lib/pages/device.dart index 9ed7a61..8caacaf 100644 --- a/lib/pages/device.dart +++ b/lib/pages/device.dart @@ -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 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 with WidgetsBindingObserver { }, ), ], - ) - else - PrimaryButton( - child: Text(AppLocalizations.of(context).noControllerUseCompanionMode), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (c) => ButtonSimulator(), - ), - ); - }, ), ], ), diff --git a/lib/pages/navigation.dart b/lib/pages/navigation.dart index 95c2697..996079b 100644 --- a/lib/pages/navigation.dart +++ b/lib/pages/navigation.dart @@ -135,6 +135,7 @@ class _NavigationState extends State { backgroundColor: Theme.of(context).colorScheme.background, trailing: buildMenuButtons( context, + _selectedPage, _isMobile ? () { setState(() { diff --git a/lib/pages/trainer.dart b/lib/pages/trainer.dart index a90a8ab..aa51f5e 100644 --- a/lib/pages/trainer.dart +++ b/lib/pages/trainer.dart @@ -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 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 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 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 with WidgetsBindingObserver { spacing: 8, children: [ PrimaryButton( + leading: Icon(Icons.computer_outlined), child: Text( AppLocalizations.of( context, @@ -217,10 +219,11 @@ class _TrainerPageState extends State with WidgetsBindingObserver { }, ), PrimaryButton( - child: Text(context.i18n.adjustControllerButtons), + leading: Icon(BCPage.customization.icon), onPressed: () { widget.goToNextPage(); }, + child: Text(context.i18n.adjustControllerButtons), ), ], ), diff --git a/lib/utils/iap/iap_manager.dart b/lib/utils/iap/iap_manager.dart index 53c7d41..1f7b5ab 100644 --- a/lib/utils/iap/iap_manager.dart +++ b/lib/utils/iap/iap_manager.dart @@ -160,8 +160,8 @@ class IAPManager { _windowsIapService?.dispose(); } - void reset() { + void reset(bool fullReset) { _windowsIapService?.reset(); - _iapService?.reset(); + _iapService?.reset(fullReset); } } diff --git a/lib/utils/iap/iap_service.dart b/lib/utils/iap/iap_service.dart index 577edc0..66018e4 100644 --- a/lib/utils/iap/iap_service.dart +++ b/lib/utils/iap/iap_service.dart @@ -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 _onPurchaseUpdate(List 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(); + } } } diff --git a/lib/utils/settings/settings.dart b/lib/utils/settings/settings.dart index 0ec4bff..02d6b01 100644 --- a/lib/utils/settings/settings.dart +++ b/lib/utils/settings/settings.dart @@ -69,8 +69,8 @@ class Settings { Future reset() async { await prefs.clear(); - IAPManager.instance.reset(); - core.actionHandler.init(null); + IAPManager.instance.reset(true); + init(); } void setTrainerApp(SupportedApp app) { diff --git a/lib/widgets/iap_status_widget.dart b/lib/widgets/iap_status_widget.dart index 847e74d..ebc4a5d 100644 --- a/lib/widgets/iap_status_widget.dart +++ b/lib/widgets/iap_status_widget.dart @@ -40,18 +40,25 @@ class _IAPStatusWidgetState extends State { 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 { 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, diff --git a/lib/widgets/logviewer.dart b/lib/widgets/logviewer.dart index ea43e4f..9eea82c 100644 --- a/lib/widgets/logviewer.dart +++ b/lib/widgets/logviewer.dart @@ -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 { ), ), ), - - 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); - }, - ), - ], - ), - ], ], ), ); diff --git a/lib/widgets/menu.dart b/lib/widgets/menu.dart index fd7b666..be04043 100644 --- a/lib/widgets/menu.dart +++ b/lib/widgets/menu.dart @@ -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 buildMenuButtons(BuildContext context, VoidCallback? openLogs) { +import '../utils/iap/iap_manager.dart'; + +List buildMenuButtons(BuildContext context, BCPage currentPage, VoidCallback? openLogs) { return [ Builder( builder: (context) { @@ -210,7 +213,7 @@ List 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), diff --git a/lib/widgets/scan.dart b/lib/widgets/scan.dart index a633e4e..a612bce 100644 --- a/lib/widgets/scan.dart +++ b/lib/widgets/scan.dart @@ -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 { ), ], ), + SizedBox(), if (!kIsWeb && (Platform.isMacOS || Platform.isWindows)) ValueListenableBuilder( valueListenable: core.mediaKeyHandler.isMediaKeyDetectionEnabled, @@ -109,25 +112,57 @@ class _ScanWidgetState extends State { }, ), 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 {