improve usability

This commit is contained in:
Jonas Bark
2025-09-25 11:03:33 +02:00
parent e2ac975c75
commit d7cee77c8b
20 changed files with 197 additions and 135 deletions

View File

@@ -163,6 +163,7 @@ class Connection {
}
void reset() {
_actionStreams.add(LogNotification('Disconnecting all devices'));
UniversalBle.stopScan();
isScanning.value = false;
for (var device in devices) {

View File

@@ -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());

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(),
),
),
],
),
);
}

View File

@@ -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,

View File

@@ -46,7 +46,7 @@ class UnsupportedPlatform extends PlatformRequirement {
}
class BluetoothScanning extends PlatformRequirement {
BluetoothScanning() : super('Bluetooth Scanning') {
BluetoothScanning() : super('Finding your Zwift® controller...') {
status = false;
}

View File

@@ -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()),
],
),
),

View File

@@ -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'),
),
),
],
],
),
);
}
}

View File

@@ -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: () {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

BIN
playstoreassets/mob1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

BIN
playstoreassets/mob2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

BIN
playstoreassets/tab1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

BIN
playstoreassets/tab2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB