diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 4be6274..fe519d5 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -75,7 +75,6 @@ jobs: echo "${{ secrets.KEYSTORE_PROPERTIES }}" > android/keystore.properties; - name: 🚀 Shorebird Patch macOS - if: false uses: shorebirdtech/shorebird-patch@v1 with: platform: macos @@ -87,7 +86,7 @@ jobs: with: platform: android release-version: latest - args: '--allow-asset-diffs --allow-native-diffs' + args: '--allow-asset-diffs' - name: 🚀 Shorebird Patch iOS uses: shorebirdtech/shorebird-patch@v1 @@ -136,7 +135,6 @@ jobs: token: ${{ secrets.TOKEN }} windows: - if: false name: Patch Windows runs-on: windows-latest diff --git a/lib/main.dart b/lib/main.dart index 8fe445b..211784e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,7 +10,6 @@ import 'package:swift_control/utils/actions/android.dart'; import 'package:swift_control/utils/actions/desktop.dart'; import 'package:swift_control/utils/actions/remote.dart'; import 'package:swift_control/utils/settings/settings.dart'; -import 'package:window_manager/window_manager.dart'; import 'bluetooth/connection.dart'; import 'utils/actions/base_actions.dart'; @@ -25,13 +24,6 @@ const screenshotMode = false; void main() async { WidgetsFlutterBinding.ensureInitialized(); - initializeActions(true); - if (actionHandler is DesktopActions) { - // Must add this line. - await windowManager.ensureInitialized(); - windowManager.setSize(Size(1280, 800)); - } - runApp(const SwiftPlayApp()); } diff --git a/lib/pages/device.dart b/lib/pages/device.dart index da3fdda..8d324f8 100644 --- a/lib/pages/device.dart +++ b/lib/pages/device.dart @@ -11,6 +11,7 @@ import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart'; import 'package:swift_control/main.dart'; import 'package:swift_control/pages/markdown.dart'; import 'package:swift_control/pages/touch_area.dart'; +import 'package:swift_control/utils/actions/base_actions.dart'; import 'package:swift_control/utils/actions/desktop.dart'; import 'package:swift_control/widgets/keymap_explanation.dart'; import 'package:swift_control/widgets/loading_widget.dart'; @@ -159,8 +160,7 @@ class _DevicePageState extends State with WidgetsBindingObserver { ), child: Column( children: [ - if (connection.devices.isEmpty) - Text('No devices connected. Go back and connect a device to get started.'), + if (connection.devices.isEmpty) Text('No devices connected. Searching...'), ...connection.devices.map( (device) => Row( children: [ diff --git a/lib/pages/requirements.dart b/lib/pages/requirements.dart index 7d58b24..1a05707 100644 --- a/lib/pages/requirements.dart +++ b/lib/pages/requirements.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; import 'package:swift_control/utils/requirements/platform.dart'; import 'package:swift_control/widgets/changelog_dialog.dart'; import 'package:swift_control/widgets/menu.dart'; @@ -21,7 +22,6 @@ class RequirementsPage extends StatefulWidget { class _RequirementsPageState extends State with WidgetsBindingObserver { int _currentStep = 0; - var _local = true; List _requirements = []; @@ -30,8 +30,6 @@ class _RequirementsPageState extends State with WidgetsBinding super.initState(); WidgetsBinding.instance.addObserver(this); - _local = kIsWeb || !Platform.isIOS; - // call after first frame WidgetsBinding.instance.addPostFrameCallback((_) { settings.init().then((_) { @@ -94,90 +92,61 @@ class _RequirementsPageState extends State with WidgetsBinding ), body: _requirements.isEmpty ? Center(child: CircularProgressIndicator()) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - if (!kIsWeb) - SwitchListTile.adaptive( - value: _local, - title: Text('Trainer app is running on this device'), - subtitle: Text('Turn off if you want to control another device, e.g. your tablet'), - onChanged: (local) { - if (Platform.isIOS) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('This platform only supports controlling trainer apps on other devices'), - ), - ); - } else { - initializeActions(local); + : Card( + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Stepper( + currentStep: _currentStep, + connectorColor: WidgetStateProperty.resolveWith( + (Set states) => Theme.of(context).colorScheme.primary, + ), + onStepContinue: _currentStep < _requirements.length + ? () { setState(() { - _local = local; - _reloadRequirements(); + _currentStep += 1; }); } - }, - ), - Expanded( - child: Card( - margin: EdgeInsets.symmetric(horizontal: 16), - child: Stepper( - currentStep: _currentStep, - connectorColor: WidgetStateProperty.resolveWith( - (Set states) => Theme.of(context).colorScheme.primary, - ), - onStepContinue: _currentStep < _requirements.length - ? () { - setState(() { - _currentStep += 1; - }); - } - : null, - onStepTapped: (step) { - if (_requirements[step].status) { - return; - } - final hasEarlierIncomplete = _requirements.indexWhere((req) => !req.status) < step; - if (hasEarlierIncomplete && !kDebugMode) { - return; - } - setState(() { - _currentStep = step; - }); - }, - controlsBuilder: (context, details) => Container(), - steps: _requirements - .mapIndexed( - (index, req) => Step( - title: Text(req.name, style: TextStyle(fontWeight: FontWeight.w600)), - subtitle: req.description != null ? Text(req.description!) : null, - content: Container( - padding: const EdgeInsets.only(top: 16.0), - alignment: Alignment.centerLeft, - child: - (index == _currentStep - ? req.build(context, () { - _reloadRequirements(); - }) - : null) ?? - ElevatedButton( - onPressed: req.status - ? null - : () => _callRequirement(req, context, () { - _reloadRequirements(); - }), - child: Text(req.name), - ), + : null, + onStepTapped: (step) { + if (_requirements[step].status && _requirements[step] is! TargetRequirement) { + return; + } + final hasEarlierIncomplete = _requirements.indexWhere((req) => !req.status) < step; + if (hasEarlierIncomplete) { + return; + } + setState(() { + _currentStep = step; + }); + }, + controlsBuilder: (context, details) => Container(), + steps: _requirements + .mapIndexed( + (index, req) => Step( + title: Text(req.name, style: TextStyle(fontWeight: FontWeight.w600)), + subtitle: req.buildDescription() ?? (req.description != null ? Text(req.description!) : null), + content: Container( + padding: const EdgeInsets.only(top: 16.0), + alignment: Alignment.centerLeft, + child: + (index == _currentStep + ? req.build(context, () { + _reloadRequirements(); + }) + : null) ?? + ElevatedButton( + onPressed: req.status + ? null + : () => _callRequirement(req, context, () { + _reloadRequirements(); + }), + child: Text(req.name), ), - state: req.status ? StepState.complete : StepState.indexed, - ), - ) - .toList(), - ), - ), - ), - ], + ), + state: req.status ? StepState.complete : StepState.indexed, + ), + ) + .toList(), + ), ), ); } @@ -189,7 +158,7 @@ class _RequirementsPageState extends State with WidgetsBinding } void _reloadRequirements() { - getRequirements(_local).then((req) { + getRequirements(settings.getLastTarget() == Target.thisDevice).then((req) { _requirements = req; _currentStep = req.indexWhere((req) => !req.status); if (mounted) { diff --git a/lib/utils/requirements/multi.dart b/lib/utils/requirements/multi.dart index fc4ea66..32aa3bd 100644 --- a/lib/utils/requirements/multi.dart +++ b/lib/utils/requirements/multi.dart @@ -79,3 +79,144 @@ class BluetoothScanning extends PlatformRequirement { return ScanWidget(); } } + +typedef BoolFunction = bool Function(); + +enum Target { + thisDevice(title: 'This device', description: 'Trainer app runs on this device', icon: Icons.devices), + iPad( + title: 'iPad', + description: 'Remotely control the trainer app on an iPad', + icon: Icons.settings_remote_outlined, + ), + android( + title: 'Android Device', + description: 'Remotely control the trainer app on an Android device', + icon: Icons.settings_remote_outlined, + ), + macOS( + title: 'Mac', + description: 'Remotely control the trainer app on a Mac', + icon: Icons.settings_remote_outlined, + ), + windows( + title: 'Windows PC', + description: 'Remotely control the trainer app on a Windows PC', + icon: Icons.settings_remote_outlined, + ); + + final String title; + final String description; + final IconData icon; + + const Target({required this.title, required this.description, required this.icon}); + + bool get isCompatible { + return switch (this) { + Target.thisDevice => !Platform.isIOS, + _ => true, + }; + } + + String? get warning { + return switch (this) { + Target.android when Platform.isAndroid => + "Download and use SwiftControl on that Android device or select 'This device'.", + Target.macOS when Platform.isMacOS => + "Download and use SwiftControl on that macOS device or select 'This device'.", + Target.windows when Platform.isWindows => + "Download and use SwiftControl on that Windows device or select 'This device'.", + Target.android => "Download and use SwiftControl on that Android device.", + Target.macOS => "Download and use SwiftControl on that macOS device.", + Target.windows => "Download and use SwiftControl on that Windows device.", + _ => null, + }; + } +} + +class TargetRequirement extends PlatformRequirement { + TargetRequirement() + : super( + 'Select Target Device', + description: 'Select your Target Device where you want to run your trainer app on', + ) { + status = false; + } + + @override + Future call(BuildContext context, VoidCallback onUpdate) async {} + + @override + Future getStatus() async { + status = settings.getLastTarget() != null; + } + + @override + Widget? build(BuildContext context, VoidCallback onUpdate) { + return DropdownMenu( + dropdownMenuEntries: Target.values.map((target) { + return DropdownMenuEntry( + value: target, + label: target.title, + enabled: target.isCompatible, + trailingIcon: Icon(target.icon), + labelWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(target.title, style: TextStyle(fontWeight: FontWeight.bold)), + Text( + target.isCompatible + ? target.description + : 'Due to iOS restrictions only controlling trainer apps on other devices is supported :(', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ); + }).toList(), + hintText: name, + initialSelection: settings.getLastTarget(), + onSelected: (target) async { + if (target != null) { + await settings.setLastTarget(target); + initializeActions(target == Target.thisDevice); + if (target.warning != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(target.warning!), + duration: Duration(seconds: 10), + ), + ); + } + onUpdate(); + } + }, + ); + } + + @override + Widget? buildDescription() { + final target = settings.getLastTarget(); + + if (target != null) { + if (target.warning != null) { + return Row( + spacing: 8, + children: [ + Icon(Icons.warning, color: Colors.red, size: 16), + Expanded( + child: Text( + settings.getLastTarget()!.warning!, + style: TextStyle(color: Colors.red), + ), + ), + ], + ); + } else { + return Text(target.title); + } + } else { + return null; + } + } +} diff --git a/lib/utils/requirements/platform.dart b/lib/utils/requirements/platform.dart index 55d36f1..8c69e17 100644 --- a/lib/utils/requirements/platform.dart +++ b/lib/utils/requirements/platform.dart @@ -22,6 +22,10 @@ abstract class PlatformRequirement { Widget? build(BuildContext context, VoidCallback onUpdate) { return null; } + + Widget? buildDescription() { + return null; + } } Future> getRequirements(bool local) async { @@ -34,15 +38,26 @@ Future> getRequirements(bool local) async { list = [BluetoothTurnedOn(), BluetoothScanning()]; } } else if (Platform.isMacOS) { - list = [BluetoothTurnedOn(), local ? KeyboardRequirement() : RemoteRequirement(), BluetoothScanning()]; + list = [ + TargetRequirement(), + BluetoothTurnedOn(), + local ? KeyboardRequirement() : RemoteRequirement(), + BluetoothScanning(), + ]; } else if (Platform.isIOS) { - list = [BluetoothTurnedOn(), RemoteRequirement(), BluetoothScanning()]; + list = [TargetRequirement(), BluetoothTurnedOn(), RemoteRequirement(), BluetoothScanning()]; } else if (Platform.isWindows) { - list = [BluetoothTurnedOn(), local ? KeyboardRequirement() : RemoteRequirement(), BluetoothScanning()]; + list = [ + TargetRequirement(), + BluetoothTurnedOn(), + local ? KeyboardRequirement() : RemoteRequirement(), + BluetoothScanning(), + ]; } else if (Platform.isAndroid) { final deviceInfoPlugin = DeviceInfoPlugin(); final deviceInfo = await deviceInfoPlugin.androidInfo; list = [ + TargetRequirement(), BluetoothTurnedOn(), local ? AccessibilityRequirement() : RemoteRequirement(), NotificationRequirement(), diff --git a/lib/utils/requirements/remote.dart b/lib/utils/requirements/remote.dart index 8c71a22..7eda6e1 100644 --- a/lib/utils/requirements/remote.dart +++ b/lib/utils/requirements/remote.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart' hide ConnectionState; import 'package:permission_handler/permission_handler.dart'; import 'package:swift_control/main.dart'; import 'package:swift_control/utils/actions/remote.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; import 'package:swift_control/utils/requirements/platform.dart'; import 'package:swift_control/widgets/small_progress_indicator.dart'; @@ -18,11 +19,28 @@ bool _isServiceAdded = false; bool _isSubscribedToEvents = false; class RemoteRequirement extends PlatformRequirement { - RemoteRequirement() : super('Connect to your other device'); + RemoteRequirement() + : super( + 'Connect to your target device', + ); @override Future call(BuildContext context, VoidCallback onUpdate) async {} + @override + Widget? buildDescription() { + return settings.getLastTarget() == null + ? null + : Text( + switch (settings.getLastTarget()) { + Target.iPad => + 'On your iPad go to Settings > Accessibility > Touch > AssistiveTouch > Pointer Devices > Devices and pair your device. Make sure AssistiveTouch is enabled.', + _ => + 'On your ${settings.getLastTarget()?.title} go into Bluetooth settings and look for SwiftControl or your machines name. Pairing is required to use the remote feature.', + }, + ); + } + Future reconnect() async { await peripheralManager.stopAdvertising(); await peripheralManager.removeAllServices(); @@ -74,8 +92,7 @@ class RemoteRequirement extends PlatformRequirement { return; } } - while (peripheralManager.state != BluetoothLowEnergyState.poweredOn && - peripheralManager.state != BluetoothLowEnergyState.unknown) { + while (peripheralManager.state != BluetoothLowEnergyState.poweredOn) { print('Waiting for peripheral manager to be powered on...'); await Future.delayed(Duration(seconds: 1)); } @@ -284,6 +301,7 @@ class _PairWidgetState extends State<_PairWidget> { Widget build(BuildContext context) { return Column( spacing: 10, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( spacing: 10, @@ -295,21 +313,9 @@ class _PairWidgetState extends State<_PairWidget> { child: Text(_isAdvertising ? 'Stop Pairing' : 'Start Pairing'), ), if (_isAdvertising || _isLoading) SizedBox(height: 20, width: 20, child: SmallProgressIndicator()), - if (kDebugMode && !screenshotMode) - ElevatedButton( - onPressed: () { - (actionHandler as RemoteActions).sendAbsMouseReport(0, 90, 90); - (actionHandler as RemoteActions).sendAbsMouseReport(1, 90, 90); - (actionHandler as RemoteActions).sendAbsMouseReport(0, 90, 90); - }, - child: Text('Test'), - ), ], ), if (_isAdvertising) ...[ - Text( - 'If your other device is an iOS device, go to Settings > Accessibility > Touch > AssistiveTouch > Pointer Devices > Devices and pair your device. Make sure AssistiveTouch is enabled.', - ), TextButton( onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md'))); diff --git a/lib/utils/settings/settings.dart b/lib/utils/settings/settings.dart index 15aac8c..f076850 100644 --- a/lib/utils/settings/settings.dart +++ b/lib/utils/settings/settings.dart @@ -4,8 +4,11 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:swift_control/utils/keymap/apps/supported_app.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; +import 'package:window_manager/window_manager.dart'; import '../../main.dart'; +import '../actions/desktop.dart'; import '../keymap/apps/custom_app.dart'; class Settings { @@ -13,6 +16,12 @@ class Settings { Future init() async { prefs = await SharedPreferences.getInstance(); + initializeActions(settings.getLastTarget() == Target.thisDevice); + + if (actionHandler is DesktopActions) { + // Must add this line. + await windowManager.ensureInitialized(); + } try { // Get screen size for migrations @@ -136,6 +145,16 @@ class Settings { return prefs.getString('last_seen_version'); } + Target? getLastTarget() { + final targetString = prefs.getString('last_target'); + if (targetString == null) return null; + return Target.values.firstOrNullWhere((e) => e.name == targetString); + } + + Future setLastTarget(Target target) async { + await prefs.setString('last_target', target.name); + } + Future setLastSeenVersion(String version) async { await prefs.setString('last_seen_version', version); } diff --git a/lib/widgets/menu.dart b/lib/widgets/menu.dart index ab63736..d70b15a 100644 --- a/lib/widgets/menu.dart +++ b/lib/widgets/menu.dart @@ -51,6 +51,29 @@ List buildMenuButtons() { ), SizedBox(width: 8), ], + PopupMenuButton( + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + child: Text('Troubleshooting Guide'), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')), + ); + }, + ), + PopupMenuItem( + child: Text('Get Support'), + onTap: () { + launchUrlString('https://github.com/jonasbark/swiftcontrol/issues'); + }, + ), + ]; + }, + icon: Icon(Icons.help_outline), + ), + SizedBox(width: 8), const MenuButton(), SizedBox(width: 8), ]; @@ -106,21 +129,7 @@ class MenuButton extends StatelessWidget { Navigator.push(context, MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'CHANGELOG.md'))); }, ), - PopupMenuItem( - child: Text('Troubleshooting Guide'), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')), - ); - }, - ), - PopupMenuItem( - child: Text('Feedback'), - onTap: () { - launchUrlString('https://github.com/jonasbark/swiftcontrol/issues'); - }, - ), + PopupMenuItem( child: Text('License'), onTap: () {