mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
ui adjustments, fixes
This commit is contained in:
@@ -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!) {
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class _CustomizeState extends State<CustomizePage> {
|
||||
);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user