diff --git a/lib/bluetooth/devices/bluetooth_device.dart b/lib/bluetooth/devices/bluetooth_device.dart index 65b8048..f8a9a89 100644 --- a/lib/bluetooth/devices/bluetooth_device.dart +++ b/lib/bluetooth/devices/bluetooth_device.dart @@ -243,7 +243,7 @@ abstract class BluetoothDevice extends BaseDevice { } }, renderChild: (isLoading, tap) => IconButton( - variance: ButtonVariance.outline, + variance: ButtonVariance.muted, icon: isLoading ? SmallProgressIndicator() : Icon(Icons.clear), onPressed: tap, ), @@ -260,7 +260,7 @@ abstract class BluetoothDevice extends BaseDevice { filled: true, fillColor: Theme.of(context).colorScheme.background, child: Basic( - title: Text('Connection Status'), + title: Text('Connection'), trailingAlignment: Alignment.centerRight, trailing: Icon(switch (isConnected) { true => Icons.bluetooth_connected_outlined, @@ -274,7 +274,7 @@ abstract class BluetoothDevice extends BaseDevice { filled: true, fillColor: Theme.of(context).colorScheme.background, child: Basic( - title: Text('Battery Level'), + title: Text('Battery'), trailingAlignment: Alignment.centerRight, trailing: Icon(switch (batteryLevel!) { >= 80 => Icons.battery_full, @@ -292,7 +292,7 @@ abstract class BluetoothDevice extends BaseDevice { filled: true, fillColor: Theme.of(context).colorScheme.background, child: Basic( - title: Text('Firmware Version'), + title: Text('Firmware'), subtitle: Row( children: [ Text('$firmwareVersion'), @@ -314,7 +314,7 @@ abstract class BluetoothDevice extends BaseDevice { filled: true, fillColor: Theme.of(context).colorScheme.background, child: Basic( - title: Text('Signal Strength'), + title: Text('Signal'), trailingAlignment: Alignment.centerRight, trailing: Icon( switch (rssi!) { diff --git a/lib/pages/configuration.dart b/lib/pages/configuration.dart index 0f3d9d4..20fd507 100644 --- a/lib/pages/configuration.dart +++ b/lib/pages/configuration.dart @@ -15,39 +15,42 @@ class _ConfigurationPageState extends State { @override Widget build(BuildContext context) { - return Column( - spacing: 26, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text.rich( - TextSpan( - children: [ - TextSpan(text: 'Need help? Click on the '), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Icon(Icons.help_outline), + return Padding( + padding: EdgeInsets.all(16), + child: Column( + spacing: 26, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan(text: 'Need help? Click on the '), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Icon(Icons.help_outline), + ), ), - ), - TextSpan(text: ' button on top and don\'t hesitate to contact us.'), - ], + TextSpan(text: ' button on top and don\'t hesitate to contact us.'), + ], + ), + ).small.muted, + Card( + child: requirement.build(context, () { + setState(() {}); + })!, ), - ).small.muted, - Card( - child: requirement.build(context, () { - setState(() {}); - })!, - ), - PrimaryButton( - onPressed: core.settings.getTrainerApp() != null && core.settings.getLastTarget() != null - ? () { - widget.onUpdate(); - } - : null, - child: Text('Continue'), - ), - ], + PrimaryButton( + onPressed: core.settings.getTrainerApp() != null && core.settings.getLastTarget() != null + ? () { + widget.onUpdate(); + } + : null, + child: Text('Continue'), + ), + ], + ), ); } } diff --git a/lib/pages/customize.dart b/lib/pages/customize.dart index 2327bd2..4f837b9 100644 --- a/lib/pages/customize.dart +++ b/lib/pages/customize.dart @@ -25,6 +25,7 @@ class _CustomizeState extends State { ); return SingleChildScrollView( + padding: EdgeInsets.all(16), child: Column( spacing: 12, mainAxisAlignment: MainAxisAlignment.start, diff --git a/lib/pages/device.dart b/lib/pages/device.dart index b4eef11..7c1a7fb 100644 --- a/lib/pages/device.dart +++ b/lib/pages/device.dart @@ -1,24 +1,13 @@ import 'dart:async'; -import 'dart:io'; -import 'package:device_auto_rotate_checker/device_auto_rotate_checker.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:swift_control/main.dart'; import 'package:swift_control/utils/core.dart'; import 'package:swift_control/widgets/scan.dart'; -import 'package:swift_control/widgets/ui/toast.dart'; import 'package:swift_control/widgets/ui/warning.dart'; -import 'package:universal_ble/universal_ble.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import '../bluetooth/devices/base_device.dart'; -import '../utils/actions/android.dart'; -import '../utils/actions/remote.dart'; -import '../utils/requirements/remote.dart'; import '../widgets/ignored_devices_dialog.dart'; class DevicePage extends StatefulWidget { @@ -31,114 +20,28 @@ class DevicePage extends StatefulWidget { class _DevicePageState extends State with WidgetsBindingObserver { late StreamSubscription _connectionStateSubscription; - bool _showAutoRotationWarning = false; - bool _showMiuiWarning = false; bool _showNameChangeWarning = false; - StreamSubscription? _autoRotateStream; @override void initState() { super.initState(); - // keep screen on - this is required for iOS to keep the bluetooth connection alive - if (!screenshotMode) { - WakelockPlus.enable(); - } _showNameChangeWarning = !core.settings.knowsAboutNameChange(); - WidgetsBinding.instance.addObserver(this); - - if (core.actionHandler is RemoteActions && - !kIsWeb && - Platform.isIOS && - (core.actionHandler as RemoteActions).isConnected) { - WidgetsBinding.instance.addPostFrameCallback((_) { - // show snackbar to inform user that the app needs to stay in foreground - showToast( - builder: (c, overlay) => - buildToast(c, overlay, title: 'To simulate touches the app needs to stay in the foreground.'), - context: context, - ); - }); - } _connectionStateSubscription = core.connection.connectionStream.listen((state) async { setState(() {}); }); - - if (!kIsWeb && Platform.isAndroid) { - DeviceAutoRotateChecker.checkAutoRotate().then((isEnabled) { - if (!isEnabled) { - setState(() { - _showAutoRotationWarning = true; - }); - } - }); - _autoRotateStream = DeviceAutoRotateChecker.autoRotateStream.listen((isEnabled) { - setState(() { - _showAutoRotationWarning = !isEnabled; - }); - }); - - // Check if device is MIUI and using local accessibility service - if (core.actionHandler is AndroidActions) { - _checkMiuiDevice(); - } - } } @override void dispose() { - WidgetsBinding.instance.removeObserver(this); - - _autoRotateStream?.cancel(); _connectionStateSubscription.cancel(); super.dispose(); } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - if (core.actionHandler is RemoteActions && Platform.isIOS && (core.actionHandler as RemoteActions).isConnected) { - UniversalBle.getBluetoothAvailabilityState().then((state) { - if (state == AvailabilityState.poweredOn && mounted) { - final requirement = RemoteRequirement(); - requirement.reconnect(); - showToast( - builder: (c, overlay) => - buildToast(c, overlay, title: 'To simulate touches the app needs to stay in the foreground.'), - context: context, - ); - } - }); - } - } - } - - Future _checkMiuiDevice() async { - try { - // Don't show if user has dismissed the warning - if (core.settings.getMiuiWarningDismissed()) { - return; - } - - final deviceInfo = await DeviceInfoPlugin().androidInfo; - final isMiui = - deviceInfo.manufacturer.toLowerCase() == 'xiaomi' || - deviceInfo.brand.toLowerCase() == 'xiaomi' || - deviceInfo.brand.toLowerCase() == 'redmi' || - deviceInfo.brand.toLowerCase() == 'poco'; - if (isMiui && mounted) { - setState(() { - _showMiuiWarning = true; - }); - } - } catch (e) { - // Silently fail if device info is not available - } - } - @override Widget build(BuildContext context) { return SingleChildScrollView( + padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 12, @@ -162,74 +65,6 @@ class _DevicePageState extends State with WidgetsBindingObserver { ), ], ), - if (_showAutoRotationWarning) - Warning( - important: false, - children: [ - Text('Enable auto-rotation on your device to make sure the app works correctly.'), - ], - ), - if (_showMiuiWarning) - Warning( - children: [ - Row( - children: [ - Icon(Icons.warning_amber), - SizedBox(width: 8), - Expanded( - child: Text( - 'MIUI Device Detected', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton.destructive( - icon: Icon(Icons.close), - onPressed: () async { - await core.settings.setMiuiWarningDismissed(true); - setState(() { - _showMiuiWarning = false; - }); - }, - ), - ], - ), - SizedBox(height: 8), - Text( - 'Your device is running MIUI, which is known to aggressively kill background services and accessibility services.', - style: TextStyle(fontSize: 14), - ), - SizedBox(height: 8), - Text( - 'To ensure BikeControl works properly:', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), - ), - Text( - '• Disable battery optimization for BikeControl', - style: TextStyle(fontSize: 14), - ), - Text( - '• Enable autostart for BikeControl', - style: TextStyle(fontSize: 14), - ), - Text( - '• Lock the app in recent apps', - style: TextStyle(fontSize: 14), - ), - SizedBox(height: 12), - IconButton.secondary( - onPressed: () async { - final url = Uri.parse('https://dontkillmyapp.com/xiaomi'); - if (await canLaunchUrl(url)) { - await launchUrl(url, mode: LaunchMode.externalApplication); - } - }, - icon: Icon(Icons.open_in_new), - trailing: Text('View Detailed Instructions'), - ), - ], - ), ScanWidget(), ...core.connection.controllerDevices.map( diff --git a/lib/pages/navigation.dart b/lib/pages/navigation.dart index fd0d541..bdf1054 100644 --- a/lib/pages/navigation.dart +++ b/lib/pages/navigation.dart @@ -41,14 +41,25 @@ class _NavigationState extends State { void initState() { super.initState(); core.connection.actionStream.listen((_) { + _updateTrainerConnectionStatus(); setState(() {}); }); + _updateTrainerConnectionStatus(); WidgetsBinding.instance.addPostFrameCallback((_) { _checkAndShowChangelog(); }); } + void _updateTrainerConnectionStatus() async { + final isConnected = await core.logic.isTrainerConnected(); + if (mounted) { + setState(() { + _isTrainerConnected = isConnected; + }); + } + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -75,6 +86,8 @@ class _NavigationState extends State { final List _tabs = BCPage.values.whereNot((e) => e == BCPage.logs).toList(); + bool _isTrainerConnected = false; + @override Widget build(BuildContext context) { return Scaffold( @@ -111,7 +124,6 @@ class _NavigationState extends State { Expanded( child: Container( alignment: Alignment.topLeft, - padding: EdgeInsets.all(16), child: AnimatedSwitcher( duration: Duration(milliseconds: 200), child: switch (_selectedPage) { @@ -268,7 +280,7 @@ class _NavigationState extends State { BCPage.configuration => core.settings.getTrainerApp() == null, BCPage.devices => core.connection.controllerDevices.isEmpty, BCPage.customization => false, - BCPage.trainer => false, + BCPage.trainer => !_isTrainerConnected, BCPage.logs => false, }; } diff --git a/lib/pages/trainer.dart b/lib/pages/trainer.dart index 9a126aa..5b8d77b 100644 --- a/lib/pages/trainer.dart +++ b/lib/pages/trainer.dart @@ -1,19 +1,29 @@ +import 'dart:async'; import 'dart:io'; +import 'package:dartx/dartx.dart'; +import 'package:device_auto_rotate_checker/device_auto_rotate_checker.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:swift_control/bluetooth/messages/notification.dart'; import 'package:swift_control/main.dart'; -import 'package:swift_control/utils/actions/android.dart'; +import 'package:swift_control/utils/actions/remote.dart'; import 'package:swift_control/utils/core.dart'; -import 'package:swift_control/utils/keymap/apps/my_whoosh.dart'; import 'package:swift_control/utils/requirements/android.dart'; import 'package:swift_control/utils/requirements/multi.dart'; import 'package:swift_control/utils/requirements/remote.dart'; import 'package:swift_control/widgets/apps/mywhoosh_link_tile.dart'; import 'package:swift_control/widgets/apps/zwift_tile.dart'; import 'package:swift_control/widgets/ui/connection_method.dart'; +import 'package:swift_control/widgets/ui/toast.dart'; import 'package:swift_control/widgets/ui/warning.dart'; +import 'package:universal_ble/universal_ble.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart' show launchUrlString; +import 'package:wakelock_plus/wakelock_plus.dart'; + +import '../utils/actions/android.dart'; class TrainerPage extends StatefulWidget { final VoidCallback onUpdate; @@ -23,13 +33,34 @@ class TrainerPage extends StatefulWidget { State createState() => _TrainerPageState(); } -class _TrainerPageState extends State { +class _TrainerPageState extends State with WidgetsBindingObserver { bool? _isRunningAndroidService; + bool _showAutoRotationWarning = false; + bool _showMiuiWarning = false; + StreamSubscription? _autoRotateStream; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); + + // keep screen on - this is required for iOS to keep the bluetooth connection alive + if (!screenshotMode) { + WakelockPlus.enable(); + } + if (!kIsWeb) { + if (core.logic.showForegroundMessage) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // show snackbar to inform user that the app needs to stay in foreground + showToast( + builder: (c, overlay) => + buildToast(c, overlay, title: 'To simulate touches the app needs to stay in the foreground.'), + context: context, + ); + }); + } + core.whooshLink.isStarted.addListener(() { if (mounted) setState(() {}); }); @@ -38,100 +69,248 @@ class _TrainerPageState extends State { if (mounted) setState(() {}); }); - if (core.settings.getZwiftEmulatorEnabled() && core.settings.getTrainerApp()?.supportsZwiftEmulation == true) { + if (core.logic.shouldStartZwiftEmulator) { core.zwiftEmulator.startAdvertising(() { if (mounted) setState(() {}); }); } - if (Platform.isAndroid && core.actionHandler is AndroidActions) { - (core.actionHandler as AndroidActions).accessibilityHandler.isRunning().then((isRunning) { + if (core.logic.canRunAndroidService) { + core.logic.isAndroidServiceRunning().then((isRunning) { + core.connection.signalNotification(LogNotification('Local Control: $isRunning')); setState(() { _isRunningAndroidService = isRunning; }); }); } + + if (Platform.isAndroid) { + DeviceAutoRotateChecker.checkAutoRotate().then((isEnabled) { + if (!isEnabled) { + setState(() { + _showAutoRotationWarning = true; + }); + } + }); + _autoRotateStream = DeviceAutoRotateChecker.autoRotateStream.listen((isEnabled) { + setState(() { + _showAutoRotationWarning = !isEnabled; + }); + }); + + // Check if device is MIUI and using local accessibility service + if (core.actionHandler is AndroidActions) { + _checkMiuiDevice(); + } + } + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + + _autoRotateStream?.cancel(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + if (core.logic.showForegroundMessage) { + UniversalBle.getBluetoothAvailabilityState().then((state) { + if (state == AvailabilityState.poweredOn && mounted) { + final requirement = RemoteRequirement(); + requirement.reconnect(); + showToast( + builder: (c, overlay) => + buildToast(c, overlay, title: 'To simulate touches the app needs to stay in the foreground.'), + context: context, + ); + } + }); + } + } + } + + Future _checkMiuiDevice() async { + try { + // Don't show if user has dismissed the warning + if (core.settings.getMiuiWarningDismissed()) { + return; + } + + final deviceInfo = await DeviceInfoPlugin().androidInfo; + final isMiui = + deviceInfo.manufacturer.toLowerCase() == 'xiaomi' || + deviceInfo.brand.toLowerCase() == 'xiaomi' || + deviceInfo.brand.toLowerCase() == 'redmi' || + deviceInfo.brand.toLowerCase() == 'poco'; + if (isMiui && mounted) { + setState(() { + _showMiuiWarning = true; + }); + } + } catch (e) { + // Silently fail if device info is not available } } @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, - children: [ - if (core.settings.getLastTarget()?.connectionType == ConnectionType.local && - (Platform.isMacOS || Platform.isWindows || Platform.isAndroid)) - Card( - child: ConnectionMethod( - title: 'Control ${core.settings.getTrainerApp()?.name} using Keyboard / Mouse / Touch', - description: - 'Enable keyboard and mouse control for better interaction with ${core.settings.getTrainerApp()?.name}.', - requirements: [Platform.isAndroid ? AccessibilityRequirement() : KeyboardRequirement()], - isStarted: _isRunningAndroidService == true, - onChange: (value) { - if (Platform.isAndroid && core.actionHandler is AndroidActions) { - (core.actionHandler as AndroidActions).accessibilityHandler.isRunning().then((isRunning) { - setState(() { - _isRunningAndroidService = isRunning; + return SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + if (_showAutoRotationWarning) + Warning( + important: false, + children: [ + Text('Enable auto-rotation on your device to make sure the app works correctly.'), + ], + ), + if (_showMiuiWarning) + Warning( + children: [ + Row( + children: [ + Icon(Icons.warning_amber), + SizedBox(width: 8), + Expanded( + child: Text('MIUI Device Detected').bold, + ), + IconButton.destructive( + icon: Icon(Icons.close), + onPressed: () async { + await core.settings.setMiuiWarningDismissed(true); + setState(() { + _showMiuiWarning = false; + }); + }, + ), + ], + ), + SizedBox(height: 8), + Text( + 'Your device is running MIUI, which is known to aggressively kill background services and accessibility services.', + style: TextStyle(fontSize: 14), + ), + SizedBox(height: 8), + Text( + 'To ensure BikeControl works properly:', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + ), + Text( + '• Disable battery optimization for BikeControl', + style: TextStyle(fontSize: 14), + ), + Text( + '• Enable autostart for BikeControl', + style: TextStyle(fontSize: 14), + ), + Text( + '• Lock the app in recent apps', + style: TextStyle(fontSize: 14), + ), + SizedBox(height: 12), + IconButton.secondary( + onPressed: () async { + final url = Uri.parse('https://dontkillmyapp.com/xiaomi'); + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } + }, + icon: Icon(Icons.open_in_new), + trailing: Text('View Detailed Instructions'), + ), + ], + ), + if (core.logic.showLocalControl) + Card( + child: ConnectionMethod( + title: + 'Control ${core.settings.getTrainerApp()?.name} using ${core.actionHandler.supportedModes.joinToString(transform: (e) => e.name)}', + description: + 'Enable keyboard and mouse control for better interaction with ${core.settings.getTrainerApp()?.name}.', + requirements: [Platform.isAndroid ? AccessibilityRequirement() : KeyboardRequirement()], + isStarted: core.logic.canRunAndroidService ? _isRunningAndroidService == true : null, + onChange: (value) { + if (core.logic.canRunAndroidService) { + core.logic.canRunAndroidService.then((isRunning) { + core.connection.signalNotification(LogNotification('Local Control: $isRunning')); + setState(() { + _isRunningAndroidService = isRunning; + }); }); - }); - } - }, - additionalChild: _isRunningAndroidService == false - ? Warning( - children: [ - Text('Accessibility Service is not running.\nFollow instructions at').xSmall, - Row( - spacing: 8, - children: [ - Expanded( - child: LinkButton( - child: Text('dontkillmyapp.com'), - onPressed: () { - launchUrlString('https://dontkillmyapp.com/'); - }, + } + }, + additionalChild: _isRunningAndroidService == false + ? Warning( + children: [ + Text('Accessibility Service is not running.\nFollow instructions at').xSmall, + Row( + spacing: 8, + children: [ + Expanded( + child: LinkButton( + child: Text('dontkillmyapp.com'), + onPressed: () { + launchUrlString('https://dontkillmyapp.com/'); + }, + ), ), - ), - IconButton.secondary( - onPressed: () { - (core.actionHandler as AndroidActions).accessibilityHandler.isRunning().then(( - isRunning, - ) { - setState(() { - _isRunningAndroidService = isRunning; + IconButton.secondary( + onPressed: () { + core.logic.isAndroidServiceRunning().then(( + isRunning, + ) { + core.connection.signalNotification(LogNotification('Local Control: $isRunning')); + setState(() { + _isRunningAndroidService = isRunning; + }); }); - }); - }, - icon: Icon(Icons.refresh), - ), - ], - ), - ], - ) - : null, + }, + icon: Icon(Icons.refresh), + ), + ], + ), + ], + ) + : null, + ), ), - ), - if (core.settings.getTrainerApp() is MyWhoosh && core.whooshLink.isCompatible(core.settings.getLastTarget()!)) - Card(child: MyWhooshLinkTile()), - if (core.settings.getTrainerApp()?.supportsZwiftEmulation == true) - Card( - child: ZwiftTile( - onUpdate: () { - setState(() {}); - }, + if (core.logic.showMyWhooshLink) Card(child: MyWhooshLinkTile()), + if (core.logic.showZwiftEmulator) + Card( + child: ZwiftTile( + onUpdate: () { + core.connection.signalNotification( + LogNotification('Zwift Emulator status changed to ${core.zwiftEmulator.isConnected.value}'), + ); + setState(() {}); + }, + ), ), + + if (core.logic.showRemote) + Card( + child: RemoteRequirement().build(context, () { + core.connection.signalNotification( + LogNotification('Remote Control changed to ${(core.actionHandler as RemoteActions).isConnected}'), + ); + })!, + ), + + PrimaryButton( + child: Text('Adjust Controller Buttons'), + onPressed: () { + widget.onUpdate(); + }, ), - - if (core.settings.getLastTarget() != Target.thisDevice) Card(child: RemoteRequirement().build(context, () {})!), - - PrimaryButton( - child: Text('Adjust Controller Buttons'), - onPressed: () { - widget.onUpdate(); - }, - ), - ], + ], + ), ); } } diff --git a/lib/utils/core.dart b/lib/utils/core.dart index 5f6228b..51542b0 100644 --- a/lib/utils/core.dart +++ b/lib/utils/core.dart @@ -1,10 +1,19 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:keypress_simulator/keypress_simulator.dart'; import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/android.dart'; import 'package:swift_control/utils/actions/base_actions.dart'; +import 'package:swift_control/utils/actions/remote.dart'; +import 'package:swift_control/utils/keymap/apps/my_whoosh.dart'; import 'package:swift_control/utils/settings/settings.dart'; import '../bluetooth/connection.dart'; import '../bluetooth/devices/link/link.dart'; +import 'requirements/multi.dart'; final core = Core(); @@ -16,4 +25,61 @@ class Core { final connection = Connection(); final zwiftEmulator = ZwiftEmulator(); + + final logic = CoreLogic(); +} + +class CoreLogic { + bool get showLocalControl { + return core.settings.getLastTarget()?.connectionType == ConnectionType.local && + (Platform.isMacOS || Platform.isWindows || Platform.isAndroid); + } + + bool get canRunAndroidService { + return Platform.isAndroid && core.actionHandler is AndroidActions; + } + + Future isAndroidServiceRunning() async { + if (canRunAndroidService) { + return (core.actionHandler as AndroidActions).accessibilityHandler.isRunning(); + } + return false; + } + + bool get shouldStartZwiftEmulator { + return core.settings.getZwiftEmulatorEnabled() && showZwiftEmulator; + } + + bool get showZwiftEmulator { + return core.settings.getTrainerApp()?.supportsZwiftEmulation == true; + } + + bool get showMyWhooshLink => + core.settings.getTrainerApp() is MyWhoosh && core.whooshLink.isCompatible(core.settings.getLastTarget()!); + + bool get showRemote => core.settings.getLastTarget() != Target.thisDevice && core.actionHandler is RemoteActions; + + bool get showForegroundMessage => + core.actionHandler is RemoteActions && + !kIsWeb && + Platform.isIOS && + (core.actionHandler as RemoteActions).isConnected; + + Future isTrainerConnected() async { + if (showLocalControl) { + if (canRunAndroidService) { + return isAndroidServiceRunning(); + } else { + return await keyPressSimulator.isAccessAllowed(); + } + } else if (showMyWhooshLink) { + return core.whooshLink.isStarted.value; + } else if (showZwiftEmulator) { + return core.zwiftEmulator.isConnected.value; + } else if (showRemote && core.actionHandler is RemoteActions) { + return (core.actionHandler as RemoteActions).isConnected; + } else { + return false; + } + } } diff --git a/lib/widgets/scan.dart b/lib/widgets/scan.dart index 3288131..6f4dfab 100644 --- a/lib/widgets/scan.dart +++ b/lib/widgets/scan.dart @@ -84,7 +84,7 @@ class _ScanWidgetState extends State { 'Scanning for devices... Make sure they are powered on and in range and not connected to another device.', ).small.muted, ), - WifiAnimation(), + SmoothWifiAnimation(), ], ), if (!kIsWeb && (Platform.isMacOS || Platform.isWindows)) diff --git a/lib/widgets/ui/connection_method.dart b/lib/widgets/ui/connection_method.dart index 929be7e..0819eca 100644 --- a/lib/widgets/ui/connection_method.dart +++ b/lib/widgets/ui/connection_method.dart @@ -45,7 +45,7 @@ class _ConnectionMethodState extends State with WidgetsBinding setState(() { _isStarted = allDone; }); - widget.onChange(true); + widget.onChange(allDone); } }); } @@ -226,7 +226,7 @@ class _PermissionListState extends State<_PermissionList> with WidgetsBindingObs spacing: 18, children: [ Text( - 'Please complete the following requirements before enabling this connection method:', + 'The following permissions are required:', style: TextStyle(fontWeight: FontWeight.bold), ), ...widget.requirements.map( diff --git a/lib/widgets/ui/wifi_animation.dart b/lib/widgets/ui/wifi_animation.dart index 34741d8..e2ece6f 100644 --- a/lib/widgets/ui/wifi_animation.dart +++ b/lib/widgets/ui/wifi_animation.dart @@ -1,15 +1,14 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; -class WifiAnimation extends StatefulWidget { - const WifiAnimation({super.key}); +class SmoothWifiAnimation extends StatefulWidget { + const SmoothWifiAnimation({super.key}); @override - State createState() => _WifiAnimationState(); + State createState() => _SmoothWifiAnimationState(); } -class _WifiAnimationState extends State with SingleTickerProviderStateMixin { +class _SmoothWifiAnimationState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; - late final Animation _index; final _animationIcons = [ Icons.wifi_1_bar, @@ -17,17 +16,27 @@ class _WifiAnimationState extends State with SingleTickerProvider Icons.wifi, ]; + int _currentIndex = 0; + @override void initState() { super.initState(); - _controller = AnimationController( - duration: const Duration(seconds: 2), - vsync: this, - )..repeat(); - _index = IntTween(begin: 0, end: _animationIcons.length - 1).animate( - CurvedAnimation(parent: _controller, curve: Curves.linear), - ); + _controller = + AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + )..addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.reverse(); + } else if (status == AnimationStatus.dismissed) { + _currentIndex = (_currentIndex + 1) % _animationIcons.length; + setState(() {}); + _controller.forward(); + } + }); + + _controller.forward(); } @override @@ -38,14 +47,15 @@ class _WifiAnimationState extends State with SingleTickerProvider @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _index, - builder: (_, __) { - return Icon( - _animationIcons[_index.value], - color: Colors.gray, - ); - }, + return AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), + child: Icon( + _animationIcons[_currentIndex], + color: Colors.gray, + key: ValueKey(_currentIndex), + size: 26, + ), ); } }