ui adjustments, fixes

This commit is contained in:
Jonas Bark
2025-11-29 14:37:43 +00:00
parent 91d0fd656e
commit d05557819e
10 changed files with 410 additions and 304 deletions

View File

@@ -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!) {

View File

@@ -15,39 +15,42 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
@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'),
),
],
),
);
}
}

View File

@@ -25,6 +25,7 @@ class _CustomizeState extends State<CustomizePage> {
);
return SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.start,

View File

@@ -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<DevicePage> with WidgetsBindingObserver {
late StreamSubscription<BaseDevice> _connectionStateSubscription;
bool _showAutoRotationWarning = false;
bool _showMiuiWarning = false;
bool _showNameChangeWarning = false;
StreamSubscription<bool>? _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<void> _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<DevicePage> 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(

View File

@@ -41,14 +41,25 @@ class _NavigationState extends State<Navigation> {
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<Navigation> {
final List<BCPage> _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<Navigation> {
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<Navigation> {
BCPage.configuration => core.settings.getTrainerApp() == null,
BCPage.devices => core.connection.controllerDevices.isEmpty,
BCPage.customization => false,
BCPage.trainer => false,
BCPage.trainer => !_isTrainerConnected,
BCPage.logs => false,
};
}

View File

@@ -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<TrainerPage> createState() => _TrainerPageState();
}
class _TrainerPageState extends State<TrainerPage> {
class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
bool? _isRunningAndroidService;
bool _showAutoRotationWarning = false;
bool _showMiuiWarning = false;
StreamSubscription<bool>? _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<TrainerPage> {
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<void> _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();
},
),
],
],
),
);
}
}

View File

@@ -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<bool> 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<bool> 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;
}
}
}

View File

@@ -84,7 +84,7 @@ class _ScanWidgetState extends State<ScanWidget> {
'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))

View File

@@ -45,7 +45,7 @@ class _ConnectionMethodState extends State<ConnectionMethod> 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(

View File

@@ -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<WifiAnimation> createState() => _WifiAnimationState();
State<SmoothWifiAnimation> createState() => _SmoothWifiAnimationState();
}
class _WifiAnimationState extends State<WifiAnimation> with SingleTickerProviderStateMixin {
class _SmoothWifiAnimationState extends State<SmoothWifiAnimation> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<int> _index;
final _animationIcons = [
Icons.wifi_1_bar,
@@ -17,17 +16,27 @@ class _WifiAnimationState extends State<WifiAnimation> 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<WifiAnimation> 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,
),
);
}
}