280 lines
8.6 KiB
Dart
280 lines
8.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import '../l10n/generated/app_localizations.dart';
|
|
import '../src/rust/api.dart' as rust;
|
|
|
|
/// Translate a [LogicalKeyboardKey] into the platform-neutral label
|
|
/// stored by the PTT binding flow.
|
|
String? pttDisplayLabelForKey(LogicalKeyboardKey k) {
|
|
if (k == LogicalKeyboardKey.space) return 'Space';
|
|
if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) {
|
|
return 'Enter';
|
|
}
|
|
if (k == LogicalKeyboardKey.tab) return 'Tab';
|
|
if (k == LogicalKeyboardKey.escape) return 'Escape';
|
|
if (k == LogicalKeyboardKey.backspace) return 'Backspace';
|
|
if (k == LogicalKeyboardKey.delete) return 'Delete';
|
|
if (k == LogicalKeyboardKey.insert) return 'Insert';
|
|
if (k == LogicalKeyboardKey.home) return 'Home';
|
|
if (k == LogicalKeyboardKey.end) return 'End';
|
|
if (k == LogicalKeyboardKey.pageUp) return 'Page Up';
|
|
if (k == LogicalKeyboardKey.pageDown) return 'Page Down';
|
|
if (k == LogicalKeyboardKey.arrowUp) return 'Arrow Up';
|
|
if (k == LogicalKeyboardKey.arrowDown) return 'Arrow Down';
|
|
if (k == LogicalKeyboardKey.arrowLeft) return 'Arrow Left';
|
|
if (k == LogicalKeyboardKey.arrowRight) return 'Arrow Right';
|
|
if (k == LogicalKeyboardKey.shift ||
|
|
k == LogicalKeyboardKey.shiftLeft ||
|
|
k == LogicalKeyboardKey.shiftRight ||
|
|
k == LogicalKeyboardKey.control ||
|
|
k == LogicalKeyboardKey.controlLeft ||
|
|
k == LogicalKeyboardKey.controlRight ||
|
|
k == LogicalKeyboardKey.alt ||
|
|
k == LogicalKeyboardKey.altLeft ||
|
|
k == LogicalKeyboardKey.altRight ||
|
|
k == LogicalKeyboardKey.meta ||
|
|
k == LogicalKeyboardKey.metaLeft ||
|
|
k == LogicalKeyboardKey.metaRight ||
|
|
k == LogicalKeyboardKey.capsLock ||
|
|
k == LogicalKeyboardKey.numLock ||
|
|
k == LogicalKeyboardKey.scrollLock) {
|
|
return null;
|
|
}
|
|
final fallback = k.keyLabel.trim();
|
|
if (fallback.isEmpty) return null;
|
|
return fallback;
|
|
}
|
|
|
|
/// Result of a successful PTT binding capture.
|
|
class CapturedBinding {
|
|
const CapturedBinding({required this.inputClass, required this.platformKey});
|
|
|
|
/// Coarse input class, safe to persist and display.
|
|
final rust.BridgePttInputClass inputClass;
|
|
|
|
/// Opaque platform-neutral key label.
|
|
final String platformKey;
|
|
}
|
|
|
|
/// 'Save bookmark' name-entry dialog.
|
|
class BookmarkNameDialog extends StatefulWidget {
|
|
/// Construct a bookmark-name dialog.
|
|
const BookmarkNameDialog({super.key, required this.initialName});
|
|
|
|
/// Initial display name.
|
|
final String initialName;
|
|
|
|
@override
|
|
State<BookmarkNameDialog> createState() => _BookmarkNameDialogState();
|
|
}
|
|
|
|
class _BookmarkNameDialogState extends State<BookmarkNameDialog> {
|
|
late final TextEditingController _ctl = TextEditingController(
|
|
text: widget.initialName,
|
|
);
|
|
|
|
@override
|
|
void dispose() {
|
|
_ctl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppL10n.of(context);
|
|
return AlertDialog(
|
|
title: Text(l10n.bookmarkAddTitle),
|
|
content: TextField(
|
|
controller: _ctl,
|
|
autofocus: true,
|
|
decoration: InputDecoration(labelText: l10n.fieldDisplayName),
|
|
onSubmitted: (_) => Navigator.of(context).pop(_ctl.text),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: Text(l10n.closeAction),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.of(context).pop(_ctl.text),
|
|
child: Text(l10n.bookmarkAddAction),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Channel-password dialog.
|
|
class ChannelPasswordDialog extends StatefulWidget {
|
|
/// Construct a channel-password dialog.
|
|
const ChannelPasswordDialog({super.key});
|
|
|
|
@override
|
|
State<ChannelPasswordDialog> createState() => _ChannelPasswordDialogState();
|
|
}
|
|
|
|
class _ChannelPasswordDialogState extends State<ChannelPasswordDialog> {
|
|
final TextEditingController _ctl = TextEditingController();
|
|
|
|
@override
|
|
void dispose() {
|
|
_ctl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppL10n.of(context);
|
|
return AlertDialog(
|
|
title: Text(l10n.channelPasswordTitle),
|
|
content: TextField(
|
|
controller: _ctl,
|
|
obscureText: true,
|
|
autofocus: true,
|
|
decoration: InputDecoration(labelText: l10n.fieldPassword),
|
|
onSubmitted: (_) => Navigator.of(context).pop(_ctl.text),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: Text(l10n.closeAction),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.of(context).pop(_ctl.text),
|
|
child: Text(l10n.connectAction),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Focus-scoped dialog that captures the next key press or mouse
|
|
/// side-button click.
|
|
class PttBindingCaptureDialog extends StatefulWidget {
|
|
/// Construct a PTT binding capture dialog.
|
|
const PttBindingCaptureDialog({super.key});
|
|
|
|
@override
|
|
State<PttBindingCaptureDialog> createState() =>
|
|
_PttBindingCaptureDialogState();
|
|
}
|
|
|
|
class _PttBindingCaptureDialogState extends State<PttBindingCaptureDialog> {
|
|
final FocusNode _focusNode = FocusNode();
|
|
String? _captured;
|
|
rust.BridgePttInputClass _capturedClass = rust.BridgePttInputClass.none;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_focusNode.requestFocus();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
|
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
|
final label = pttDisplayLabelForKey(event.logicalKey);
|
|
if (label == null) return KeyEventResult.ignored;
|
|
setState(() {
|
|
_captured = label;
|
|
_capturedClass = rust.BridgePttInputClass.keyboard;
|
|
});
|
|
return KeyEventResult.handled;
|
|
}
|
|
|
|
void _captureMouseSideButton(int button) {
|
|
setState(() {
|
|
_captured = 'mouse-side-button:$button';
|
|
_capturedClass = rust.BridgePttInputClass.mouseSideButton;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppL10n.of(context);
|
|
final theme = Theme.of(context);
|
|
return AlertDialog(
|
|
title: Text(l10n.pttConfigureTitle),
|
|
content: SizedBox(
|
|
width: 360,
|
|
child: Focus(
|
|
focusNode: _focusNode,
|
|
onKeyEvent: _onKeyEvent,
|
|
autofocus: true,
|
|
child: Listener(
|
|
behavior: HitTestBehavior.opaque,
|
|
onPointerDown: (e) {
|
|
const int back = 0x08;
|
|
const int forward = 0x10;
|
|
if (e.buttons == back || e.buttons == forward) {
|
|
_captureMouseSideButton(e.buttons);
|
|
}
|
|
},
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.pttConfigurePrompt,
|
|
style: theme.textTheme.bodyMedium,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 12,
|
|
horizontal: 16,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
_captured == null
|
|
? l10n.pttConfigureWaiting
|
|
: '${l10n.pttConfigureCaptured}: $_captured',
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
fontFamily: 'monospace',
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
l10n.pttConfigurePrivacyNote,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: Text(l10n.closeAction),
|
|
),
|
|
FilledButton(
|
|
onPressed: _captured == null
|
|
? null
|
|
: () => Navigator.of(context).pop(
|
|
CapturedBinding(
|
|
inputClass: _capturedClass,
|
|
platformKey: _captured!,
|
|
),
|
|
),
|
|
child: Text(l10n.pttConfigureSaveAction),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|