improve usability
@@ -163,6 +163,7 @@ class Connection {
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_actionStreams.add(LogNotification('Disconnecting all devices'));
|
||||
UniversalBle.stopScan();
|
||||
isScanning.value = false;
|
||||
for (var device in devices) {
|
||||
|
||||
@@ -21,7 +21,9 @@ import '../messages/notification.dart';
|
||||
|
||||
abstract class BaseDevice {
|
||||
final BleDevice scanResult;
|
||||
BaseDevice(this.scanResult);
|
||||
final List<ZwiftButton> availableButtons;
|
||||
|
||||
BaseDevice(this.scanResult, {required this.availableButtons});
|
||||
|
||||
final zapEncryption = ZapCrypto(LocalKeyProvider());
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import '../messages/click_notification.dart';
|
||||
|
||||
class ZwiftClick extends BaseDevice {
|
||||
ZwiftClick(super.scanResult);
|
||||
ZwiftClick(super.scanResult) : super(availableButtons: [ZwiftButton.shiftUpRight, ZwiftButton.shiftDownLeft]);
|
||||
|
||||
ClickNotification? _lastClickNotification;
|
||||
|
||||
|
||||
@@ -8,7 +8,25 @@ import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import '../ble.dart';
|
||||
|
||||
class ZwiftPlay extends BaseDevice {
|
||||
ZwiftPlay(super.scanResult);
|
||||
ZwiftPlay(super.scanResult)
|
||||
: super(
|
||||
availableButtons: [
|
||||
ZwiftButton.y,
|
||||
ZwiftButton.z,
|
||||
ZwiftButton.a,
|
||||
ZwiftButton.b,
|
||||
ZwiftButton.onOffRight,
|
||||
ZwiftButton.sideButtonRight,
|
||||
ZwiftButton.paddleRight,
|
||||
ZwiftButton.navigationUp,
|
||||
ZwiftButton.navigationLeft,
|
||||
ZwiftButton.navigationRight,
|
||||
ZwiftButton.navigationDown,
|
||||
ZwiftButton.onOffLeft,
|
||||
ZwiftButton.sideButtonLeft,
|
||||
ZwiftButton.paddleLeft,
|
||||
],
|
||||
);
|
||||
|
||||
PlayNotification? _lastControllerNotification;
|
||||
|
||||
|
||||
@@ -7,7 +7,29 @@ import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import '../ble.dart';
|
||||
|
||||
class ZwiftRide extends BaseDevice {
|
||||
ZwiftRide(super.scanResult);
|
||||
ZwiftRide(super.scanResult)
|
||||
: super(
|
||||
availableButtons: [
|
||||
ZwiftButton.navigationLeft,
|
||||
ZwiftButton.navigationRight,
|
||||
ZwiftButton.navigationUp,
|
||||
ZwiftButton.navigationDown,
|
||||
ZwiftButton.a,
|
||||
ZwiftButton.b,
|
||||
ZwiftButton.y,
|
||||
ZwiftButton.z,
|
||||
ZwiftButton.shiftUpLeft,
|
||||
ZwiftButton.shiftDownLeft,
|
||||
ZwiftButton.shiftUpRight,
|
||||
ZwiftButton.shiftDownRight,
|
||||
ZwiftButton.powerUpLeft,
|
||||
ZwiftButton.powerUpRight,
|
||||
ZwiftButton.onOffLeft,
|
||||
ZwiftButton.onOffRight,
|
||||
ZwiftButton.paddleLeft,
|
||||
ZwiftButton.paddleRight,
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
String get customServiceId => BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID;
|
||||
|
||||
@@ -64,10 +64,14 @@ class _DevicePageState extends State<DevicePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text('Connected Devices:', style: Theme.of(context).textTheme.titleMedium),
|
||||
Text(
|
||||
'Devices:\n${connection.devices.joinToString(separator: '\n', transform: (it) {
|
||||
return "${it.device.name ?? it.runtimeType}: ${it.isConnected ? 'Connected' : 'Not connected'}${it.batteryLevel != null ? ' - Battery Level: ${it.batteryLevel}%' : ''}";
|
||||
})}',
|
||||
connection.devices.joinToString(
|
||||
separator: '\n',
|
||||
transform: (it) {
|
||||
return "${it.device.name ?? it.runtimeType}: ${it.isConnected ? 'Connected' : 'Not connected'}${it.batteryLevel != null ? ' - Battery Level: ${it.batteryLevel}%' : ''}";
|
||||
},
|
||||
),
|
||||
),
|
||||
Divider(color: Theme.of(context).colorScheme.primary, height: 30),
|
||||
if (!kIsWeb)
|
||||
@@ -78,6 +82,7 @@ class _DevicePageState extends State<DevicePage> {
|
||||
children: [
|
||||
Flex(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
direction: MediaQuery.sizeOf(context).width > 600 ? Axis.horizontal : Axis.vertical,
|
||||
spacing: 8,
|
||||
children: [
|
||||
@@ -87,7 +92,7 @@ class _DevicePageState extends State<DevicePage> {
|
||||
SupportedApp.supportedApps
|
||||
.map((app) => DropdownMenuEntry<SupportedApp>(value: app, label: app.name))
|
||||
.toList(),
|
||||
label: Text('Select Keymap'),
|
||||
label: Text('Select Keymap / app'),
|
||||
onSelected: (app) async {
|
||||
if (app == null) {
|
||||
return;
|
||||
@@ -110,39 +115,40 @@ class _DevicePageState extends State<DevicePage> {
|
||||
hintText: 'Select your Keymap',
|
||||
),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (actionHandler.supportedApp! is! CustomApp) {
|
||||
final customApp = CustomApp();
|
||||
if (actionHandler.supportedApp != null)
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (actionHandler.supportedApp! is! CustomApp) {
|
||||
final customApp = CustomApp();
|
||||
|
||||
actionHandler.supportedApp!.keymap.keyPairs.forEachIndexed((pair, index) {
|
||||
pair.buttons.forEachIndexed((button, indexB) {
|
||||
customApp.setKey(
|
||||
button,
|
||||
physicalKey: pair.physicalKey!,
|
||||
logicalKey: pair.logicalKey,
|
||||
isLongPress: pair.isLongPress,
|
||||
touchPosition:
|
||||
pair.touchPosition != Offset.zero
|
||||
? pair.touchPosition
|
||||
: Offset(((indexB + 1)) * 100, 200 + (index * 100)),
|
||||
);
|
||||
actionHandler.supportedApp!.keymap.keyPairs.forEachIndexed((pair, index) {
|
||||
pair.buttons.forEachIndexed((button, indexB) {
|
||||
customApp.setKey(
|
||||
button,
|
||||
physicalKey: pair.physicalKey!,
|
||||
logicalKey: pair.logicalKey,
|
||||
isLongPress: pair.isLongPress,
|
||||
touchPosition:
|
||||
pair.touchPosition != Offset.zero
|
||||
? pair.touchPosition
|
||||
: Offset(((indexB + 1)) * 100, 200 + (index * 100)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
actionHandler.supportedApp = customApp;
|
||||
settings.setApp(customApp);
|
||||
}
|
||||
final result = await Navigator.of(
|
||||
context,
|
||||
).push<bool>(MaterialPageRoute(builder: (_) => TouchAreaSetupPage()));
|
||||
if (result == true && actionHandler.supportedApp is CustomApp) {
|
||||
settings.setApp(actionHandler.supportedApp!);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Text('Customize Keymap'),
|
||||
),
|
||||
actionHandler.supportedApp = customApp;
|
||||
settings.setApp(customApp);
|
||||
}
|
||||
final result = await Navigator.of(
|
||||
context,
|
||||
).push<bool>(MaterialPageRoute(builder: (_) => TouchAreaSetupPage()));
|
||||
if (result == true && actionHandler.supportedApp is CustomApp) {
|
||||
settings.setApp(actionHandler.supportedApp!);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Text('Customize Keymap'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (actionHandler.supportedApp != null)
|
||||
|
||||
@@ -72,55 +72,68 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
body:
|
||||
_requirements.isEmpty
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: Stepper(
|
||||
currentStep: _currentStep,
|
||||
connectorColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) => Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onStepContinue:
|
||||
_currentStep < _requirements.length
|
||||
? () {
|
||||
setState(() {
|
||||
_currentStep += 1;
|
||||
});
|
||||
: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0),
|
||||
child: Text(
|
||||
'Please complete the following requirements to make the app work correctly:',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Stepper(
|
||||
currentStep: _currentStep,
|
||||
connectorColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) => Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onStepContinue:
|
||||
_currentStep < _requirements.length
|
||||
? () {
|
||||
setState(() {
|
||||
_currentStep += 1;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
onStepTapped: (step) {
|
||||
if (_requirements[step].status) {
|
||||
return;
|
||||
}
|
||||
: null,
|
||||
onStepTapped: (step) {
|
||||
if (_requirements[step].status) {
|
||||
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),
|
||||
content: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
alignment: Alignment.centerLeft,
|
||||
child:
|
||||
(index == _currentStep
|
||||
? req.build(context, () {
|
||||
_reloadRequirements();
|
||||
})
|
||||
: null) ??
|
||||
ElevatedButton(
|
||||
onPressed: req.status ? null : () => _callRequirement(req),
|
||||
child: Text(req.name),
|
||||
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),
|
||||
content: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
alignment: Alignment.centerLeft,
|
||||
child:
|
||||
(index == _currentStep
|
||||
? req.build(context, () {
|
||||
_reloadRequirements();
|
||||
})
|
||||
: null) ??
|
||||
ElevatedButton(
|
||||
onPressed: req.status ? null : () => _callRequirement(req),
|
||||
child: Text(req.name),
|
||||
),
|
||||
),
|
||||
),
|
||||
state: req.status ? StepState.complete : StepState.indexed,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
state: req.status ? StepState.complete : StepState.indexed,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,9 +45,7 @@ class _ScanWidgetState extends State<ScanWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(minHeight: 200),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(16),
|
||||
shrinkWrap: true,
|
||||
child: Column(
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: connection.isScanning,
|
||||
|
||||
@@ -46,7 +46,7 @@ class UnsupportedPlatform extends PlatformRequirement {
|
||||
}
|
||||
|
||||
class BluetoothScanning extends PlatformRequirement {
|
||||
BluetoothScanning() : super('Bluetooth Scanning') {
|
||||
BluetoothScanning() : super('Finding your Zwift® controller...') {
|
||||
status = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,16 @@ class KeymapExplanation extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keyboardGroups = keymap.keyPairs
|
||||
final connectedDevice = connection.devices.firstOrNull;
|
||||
|
||||
final availableKeypairs = keymap.keyPairs.filter(
|
||||
(e) => connectedDevice?.availableButtons.containsAny(e.buttons) == true,
|
||||
);
|
||||
|
||||
final keyboardGroups = availableKeypairs
|
||||
.filter((e) => e.physicalKey != null)
|
||||
.groupBy((element) => '${element.physicalKey}-${element.isLongPress}');
|
||||
final touchGroups = keymap.keyPairs
|
||||
final touchGroups = availableKeypairs
|
||||
.filter((e) => e.physicalKey == null && e.touchPosition != Offset.zero)
|
||||
.groupBy((element) => '${element.touchPosition}-${element.isLongPress}');
|
||||
|
||||
@@ -33,7 +39,7 @@ class KeymapExplanation extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Text(
|
||||
'Button on your ${connection.devices.firstOrNull?.device.name ?? connection.devices.firstOrNull?.runtimeType}',
|
||||
'Button on your ${connectedDevice?.device.name ?? connectedDevice?.runtimeType}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
@@ -57,7 +63,8 @@ class KeymapExplanation extends StatelessWidget {
|
||||
children: [
|
||||
for (final keyPair in pair.value)
|
||||
for (final button in keyPair.buttons)
|
||||
IntrinsicWidth(child: _KeyWidget(label: button.name.splitByUpperCase())),
|
||||
if (connectedDevice?.availableButtons.contains(button) == true)
|
||||
IntrinsicWidth(child: _KeyWidget(label: button.name.splitByUpperCase())),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -84,7 +91,9 @@ class KeymapExplanation extends StatelessWidget {
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final keyPair in pair.value)
|
||||
for (final button in keyPair.buttons) _KeyWidget(label: button.name.splitByUpperCase()),
|
||||
for (final button in keyPair.buttons)
|
||||
if (connectedDevice?.availableButtons.contains(button) == true)
|
||||
_KeyWidget(label: button.name.splitByUpperCase()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -48,52 +48,45 @@ class _LogviewerState extends State<LogViewer> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
SelectionArea(
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children:
|
||||
_actions
|
||||
.map(
|
||||
(action) => Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: action.date.toString().split(" ").last,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFamily: "monospace",
|
||||
fontFamilyFallback: <String>["Courier"],
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " ${action.entry}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
return SelectionArea(
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children: [
|
||||
..._actions.map(
|
||||
(action) => Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: action.date.toString().split(" ").last,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFamily: "monospace",
|
||||
fontFamilyFallback: <String>["Courier"],
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " ${action.entry}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_actions.clear();
|
||||
setState(() {});
|
||||
},
|
||||
icon: Icon(Icons.clear),
|
||||
child: Text('Clear Log'),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ List<Widget> buildMenuButtons() {
|
||||
launchUrlString(link);
|
||||
},
|
||||
),
|
||||
if (!kIsWeb && Platform.isAndroid && !isFromPlayStore && false)
|
||||
if (!kIsWeb && Platform.isAndroid && !isFromPlayStore)
|
||||
PopupMenuItem(
|
||||
child: Text('by buying the app from Play Store'),
|
||||
onTap: () {
|
||||
|
||||
|
Before Width: | Height: | Size: 156 KiB |
BIN
playstoreassets/mob1.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 182 KiB |
BIN
playstoreassets/mob2.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 213 KiB |
BIN
playstoreassets/tab1.png
Normal file
|
After Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 331 KiB |
BIN
playstoreassets/tab2.png
Normal file
|
After Width: | Height: | Size: 234 KiB |