feat: integrate chat voice and diagnostics client
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user