refactoring #5

This commit is contained in:
Jonas Bark
2025-10-26 19:27:35 +01:00
parent 6bd41d9a54
commit ec2ed4e6c5
6 changed files with 120 additions and 65 deletions

View File

@@ -285,7 +285,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
if (app == null) {
return;
} else if (app.name == 'New') {
final profileName = await KeypadManager().showNewProfileDialog(context);
final profileName = await KeymapManager().showNewProfileDialog(context);
if (profileName != null && profileName.isNotEmpty) {
final customApp = CustomApp(profileName: profileName);
actionHandler.supportedApp = customApp;
@@ -321,13 +321,13 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
IconButton(
onPressed: () async {
final currentProfile = actionHandler.supportedApp?.name;
final action = await KeypadManager().showManageProfileDialog(
final action = await KeymapManager().showManageProfileDialog(
context,
currentProfile,
);
if (action != null) {
if (action == 'rename') {
final newName = await KeypadManager().showRenameProfileDialog(
final newName = await KeymapManager().showRenameProfileDialog(
context,
currentProfile!,
);
@@ -347,7 +347,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
setState(() {});
}
} else if (action == 'duplicate') {
final newName = await KeypadManager().duplicate(
final newName = await KeymapManager().duplicate(
context,
currentProfile!,
);
@@ -357,7 +357,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
setState(() {});
}
} else if (action == 'delete') {
final confirmed = await KeypadManager().showDeleteConfirmDialog(
final confirmed = await KeymapManager().showDeleteConfirmDialog(
context,
currentProfile!,
);
@@ -367,7 +367,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
setState(() {});
}
} else if (action == 'import') {
final jsonData = await KeypadManager().showImportDialog(context);
final jsonData = await KeymapManager().showImportDialog(context);
if (jsonData != null && jsonData.isNotEmpty) {
final success = await settings.importCustomAppProfile(jsonData);
if (mounted) {

View File

@@ -349,7 +349,7 @@ class KeypairExplanation extends StatelessWidget {
)
else
Icon(keyPair.icon),
if (keyPair.physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.media)) ...[
if (keyPair.isSpecialKey && actionHandler.supportedModes.contains(SupportedMode.media))
_KeyWidget(
label: switch (keyPair.physicalKey) {
PhysicalKeyboardKey.mediaPlayPause => 'Play/Pause',
@@ -358,8 +358,12 @@ class KeypairExplanation extends StatelessWidget {
PhysicalKeyboardKey.mediaTrackNext => 'Next',
PhysicalKeyboardKey.audioVolumeUp => 'Volume Up',
PhysicalKeyboardKey.audioVolumeDown => 'Volume Down',
_ => keyPair.logicalKey?.keyLabel ?? 'Unknown',
_ => 'Unknown',
},
)
else if (keyPair.physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.keyboard)) ...[
_KeyWidget(
label: keyPair.logicalKey?.keyLabel ?? 'Unknown',
),
if (keyPair.isLongPress) Text('long\npress', style: TextStyle(fontSize: 10)),
] else ...[

View File

@@ -8,6 +8,7 @@ import 'package:swift_control/main.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import '../actions/base_actions.dart';
import 'apps/custom_app.dart';
class Keymap {
static Keymap custom = Keymap(keyPairs: []);
@@ -46,6 +47,10 @@ class Keymap {
void addKeyPair(KeyPair keyPair) {
keyPairs.add(keyPair);
_updateStream.add(null);
if (actionHandler.supportedApp is CustomApp) {
settings.setApp(actionHandler.supportedApp!);
}
}
}
@@ -126,8 +131,9 @@ class KeyPair {
: Offset.zero;
final buttons = decoded['actions']
.map<ControllerButton?>((e) => ControllerButton.values.firstOrNullWhere((element) => element.name == e))
.where((e) => e != null)
.map<ControllerButton>(
(e) => ControllerButton.values.firstOrNullWhere((element) => element.name == e) ?? ControllerButton(e),
)
.cast<ControllerButton>()
.toList();
if (buttons.isEmpty) {

View File

@@ -5,15 +5,15 @@ import 'package:swift_control/main.dart';
import 'apps/custom_app.dart';
class KeypadManager {
class KeymapManager {
// Singleton instance
static final KeypadManager _instance = KeypadManager._internal();
static final KeymapManager _instance = KeymapManager._internal();
// Private constructor
KeypadManager._internal();
KeymapManager._internal();
// Factory constructor to return the singleton instance
factory KeypadManager() {
factory KeymapManager() {
return _instance;
}
@@ -104,7 +104,7 @@ class KeypadManager {
return showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: Text('Duplicate Profile'),
title: Text('Create new custom profile by duplicating "$currentName"'),
content: TextField(
controller: controller,
decoration: InputDecoration(labelText: 'New Profile Name'),

View File

@@ -8,6 +8,7 @@ import 'package:swift_control/pages/device.dart';
import 'package:swift_control/utils/actions/base_actions.dart';
import 'package:swift_control/utils/keymap/apps/custom_app.dart';
import 'package:swift_control/utils/keymap/keymap.dart';
import 'package:swift_control/utils/keymap/manager.dart';
import 'package:swift_control/widgets/button_widget.dart';
import 'package:swift_control/widgets/custom_keymap_selector.dart';
@@ -33,6 +34,17 @@ class _KeymapExplanationState extends State<KeymapExplanation> {
});
}
@override
void didUpdateWidget(KeymapExplanation oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.keymap != widget.keymap) {
_updateStreamListener.cancel();
_updateStreamListener = widget.keymap.updateStream.listen((_) {
setState(() {});
});
}
}
@override
void dispose() {
super.dispose();
@@ -41,11 +53,8 @@ class _KeymapExplanationState extends State<KeymapExplanation> {
@override
Widget build(BuildContext context) {
final allAvailableButtons = connection.devices.flatMap((e) => e.availableButtons).distinct();
final availableKeypairs = widget.keymap.keyPairs.filter(
(e) => allAvailableButtons.containsAny(e.buttons),
);
final availableKeypairs = widget.keymap.keyPairs;
final allAvailableButtons = connection.devices.flatMap((d) => d.availableButtons);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -91,7 +100,8 @@ class _KeymapExplanationState extends State<KeymapExplanation> {
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final button in keyPair.buttons) IntrinsicWidth(child: ButtonWidget(button: button)),
for (final button in keyPair.buttons.filter((b) => allAvailableButtons.contains(b)))
IntrinsicWidth(child: ButtonWidget(button: button)),
],
),
),
@@ -225,36 +235,46 @@ class _ButtonEditor extends StatelessWidget {
else
Text('No action assigned'),
PopupMenuButton<PhysicalKeyboardKey>(
enabled: true,
itemBuilder: (context) => [
if (actions.length > 1) ...actions,
PopupMenuItem<PhysicalKeyboardKey>(
value: null,
onTap: () {
keyPair.isLongPress = !keyPair.isLongPress;
onUpdate();
},
child: CheckboxListTile(
value: keyPair.isLongPress,
onChanged: (value) {
keyPair.isLongPress = value ?? false;
if (actionHandler.supportedApp is CustomApp)
PopupMenuButton<PhysicalKeyboardKey>(
enabled: true,
itemBuilder: (context) => [
if (actions.length > 1) ...actions,
PopupMenuItem<PhysicalKeyboardKey>(
value: null,
onTap: () {
keyPair.isLongPress = !keyPair.isLongPress;
onUpdate();
Navigator.of(context).pop();
},
title: const Text('Long Press Mode (vs. repeating)'),
),
),
],
onSelected: (key) {
keyPair.physicalKey = key;
keyPair.logicalKey = null;
child: CheckboxListTile(
value: keyPair.isLongPress,
onChanged: (value) {
keyPair.isLongPress = value ?? false;
onUpdate();
},
icon: Icon(Icons.edit),
),
onUpdate();
Navigator.of(context).pop();
},
title: const Text('Long Press Mode (vs. repeating)'),
),
),
],
onSelected: (key) {
keyPair.physicalKey = key;
keyPair.logicalKey = null;
onUpdate();
},
icon: Icon(Icons.edit),
)
else
IconButton(
onPressed: () async {
final currentProfile = actionHandler.supportedApp!.name;
await KeymapManager().duplicate(context, currentProfile);
onUpdate();
},
icon: Icon(Icons.edit),
),
],
);
}

View File

@@ -1,9 +1,15 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/widgets/button_widget.dart';
import '../bluetooth/messages/notification.dart';
/// A developer overlay that visualizes touches and keyboard events.
/// - Touch dots appear where you touch and fade out over [touchRevealDuration].
@@ -40,6 +46,7 @@ class Testbed extends StatefulWidget {
class _TestbedState extends State<Testbed> with SingleTickerProviderStateMixin {
late final Ticker _ticker;
late StreamSubscription<BaseNotification> _actionSubscription;
// ----- Touch tracking -----
final Map<int, _TouchSample> _active = <int, _TouchSample>{};
@@ -55,6 +62,25 @@ class _TestbedState extends State<Testbed> with SingleTickerProviderStateMixin {
void initState() {
super.initState();
_focusNode = FocusNode(debugLabel: 'TestbedFocus', canRequestFocus: true, skipTraversal: true);
_actionSubscription = connection.actionStream.listen((data) async {
if (!mounted) {
return;
}
if (data is ButtonNotification) {
for (final button in data.buttonsClicked) {
final sample = _KeySample(
button: button,
text: '🔘 ${button.name}',
timestamp: DateTime.now(),
);
_keys.insert(0, sample);
if (_keys.length > widget.maxKeyboardEvents) {
_keys.removeLast();
}
}
setState(() {});
}
});
_ticker = createTicker((_) {
// Cull expired touch and key samples.
@@ -215,10 +241,9 @@ class _TouchesPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint =
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2;
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2;
for (final s in samples) {
final age = now.difference(s.timestamp);
@@ -242,17 +267,15 @@ class _TouchesPainter extends CustomPainter {
canvas.drawCircle(s.position, rOuter, paint);
// Inner fill (stronger)
final fill =
Paint()
..style = PaintingStyle.fill
..color = color.withOpacity(0.35 + 0.35 * fade);
final fill = Paint()
..style = PaintingStyle.fill
..color = color.withOpacity(0.35 + 0.35 * fade);
canvas.drawCircle(s.position, rInner, fill);
// Tiny center dot for precision
final center =
Paint()
..style = PaintingStyle.fill
..color = color.withOpacity(0.9 * fade);
final center = Paint()
..style = PaintingStyle.fill
..color = color.withOpacity(0.9 * fade);
canvas.drawCircle(s.position, 2.5, center);
}
}
@@ -269,7 +292,8 @@ class _TouchesPainter extends CustomPainter {
// ===== Keyboard overlay =====
class _KeySample {
_KeySample({required this.text, required this.timestamp});
_KeySample({required this.text, required this.timestamp, this.button});
final ControllerButton? button;
final String text;
final DateTime timestamp;
}
@@ -297,7 +321,7 @@ class _KeyboardOverlay extends StatelessWidget {
children: [
for (final item in items)
_KeyboardToast(
text: item.text,
item: item,
age: now.difference(item.timestamp),
duration: duration,
badgeColor: badgeColor,
@@ -310,14 +334,14 @@ class _KeyboardOverlay extends StatelessWidget {
class _KeyboardToast extends StatelessWidget {
const _KeyboardToast({
required this.text,
required this.item,
required this.age,
required this.duration,
required this.badgeColor,
required this.textStyle,
});
final String text;
final _KeySample item;
final Duration age;
final Duration duration;
final Color badgeColor;
@@ -329,13 +353,14 @@ class _KeyboardToast extends StatelessWidget {
final fade = 1.0 - t;
return Material(
color: Colors.transparent,
child: Opacity(
opacity: fade,
child: Container(
margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(color: badgeColor, borderRadius: BorderRadius.circular(12)),
child: Text(text, style: textStyle),
child: item.button != null ? ButtonWidget(button: item.button!) : Text(item.text, style: textStyle),
),
),
);