Files
meshcore-open/lib/screens/repeater_cli_screen.dart
T
zjs81 51d6210920 Add shared UI components for mesh application
- Introduced `mesh_ui.dart` with reusable widgets including SectionHeader, MeshCard, StatusChip, StatTile, AvatarCircle, SignalBars, RouteChip, PulseDot, BottomSheetHeader, ErrorRetryCard, and ListEntrance.
- Implemented `path_map_ui.dart` for path map screens, featuring path distance calculations, playback controls, and a summary list of observed paths.
- Created `themed_map_tile_layer.dart` for shared cached map tiles with automatic dark-mode treatment.
2026-06-12 21:04:02 -07:00

822 lines
34 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../theme/mesh_theme.dart';
import '../widgets/debug_frame_viewer.dart';
import '../services/repeater_command_service.dart';
import '../widgets/routing_sheet.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterCliScreen extends StatefulWidget {
final Contact repeater;
final String password;
const RepeaterCliScreen({
super.key,
required this.repeater,
required this.password,
});
@override
State<RepeaterCliScreen> createState() => _RepeaterCliScreenState();
}
class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
final TextEditingController _commandController = TextEditingController();
final FocusNode _commandFocusNode = FocusNode();
final ScrollController _scrollController = ScrollController();
final List<Map<String, String>> _commandHistory = [];
int _historyIndex = -1;
StreamSubscription<Uint8List>? _frameSubscription;
RepeaterCommandService? _commandService;
late final List<Map<String, String>> _quickCommands = [
{'labelKey': 'advertise', 'command': 'advert'},
{'labelKey': 'getName', 'command': 'get name'},
{'labelKey': 'getRadio', 'command': 'get radio'},
{'labelKey': 'getTx', 'command': 'get tx'},
{'labelKey': 'discovery', 'command': 'discover.neighbors'},
{'labelKey': 'neighbors', 'command': 'neighbors'},
{'labelKey': 'version', 'command': 'ver'},
{'labelKey': 'clock', 'command': 'clock'},
{'labelKey': 'clock sync', 'command': 'clock sync'},
];
@override
void initState() {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_setupMessageListener();
}
@override
void dispose() {
_frameSubscription?.cancel();
_commandService?.dispose();
_commandController.dispose();
_commandFocusNode.dispose();
_scrollController.dispose();
super.dispose();
}
void _setupMessageListener() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
if (frame[0] == respCodeContactMsgRecv ||
frame[0] == respCodeContactMsgRecvV3) {
_handleTextMessageResponse(frame);
}
});
}
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) {
if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
);
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
}
void _handleTextMessageResponse(Uint8List frame) {
final parsed = parseContactMessageText(frame);
if (parsed == null) return;
if (!_matchesRepeaterPrefix(parsed.senderPrefix)) return;
_commandService?.handleResponse(widget.repeater, parsed.text);
}
bool _matchesRepeaterPrefix(Uint8List prefix) {
final target = widget.repeater.publicKey;
if (target.length < 6 || prefix.length < 6) return false;
for (int i = 0; i < 6; i++) {
if (prefix[i] != target[i]) return false;
}
return true;
}
void _sendCommand({bool showDebug = false}) async {
final command = _commandController.text.trim();
if (command.isEmpty) return;
setState(() {
_commandHistory.add({
'type': 'command',
'text': command,
'timestamp': DateTime.now().toString(),
});
});
if (showDebug && mounted) {
final frame = buildSendCliCommandFrame(
widget.repeater.publicKey,
command,
);
DebugFrameViewer.showFrameDebug(
context,
frame,
context.l10n.repeater_cliCommandFrameTitle,
);
}
try {
if (_commandService != null) {
final connector = Provider.of<MeshCoreConnector>(
context,
listen: false,
);
final repeater = _resolveRepeater(connector);
final response = await _commandService!.sendCommand(
repeater,
command,
retries: 1,
);
if (mounted) {
setState(() {
_commandHistory.add({
'type': 'response',
'text': response,
'timestamp': DateTime.now().toString(),
});
});
}
}
} catch (e) {
if (mounted) {
setState(() {
_commandHistory.add({
'type': 'response',
'text': context.l10n.repeater_cliCommandError(e.toString()),
'timestamp': DateTime.now().toString(),
});
});
}
}
_commandController.clear();
_historyIndex = -1;
_commandFocusNode.requestFocus();
Future.delayed(const Duration(milliseconds: 100), () {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
});
}
void _useQuickCommand(String command) {
_commandController.text = command;
_sendCommand();
}
void _navigateHistory(bool up) {
final commands = _commandHistory
.where((entry) => entry['type'] == 'command')
.toList()
.reversed
.toList();
if (commands.isEmpty) return;
if (up) {
if (_historyIndex < commands.length - 1) {
_historyIndex++;
}
} else {
if (_historyIndex > 0) {
_historyIndex--;
} else {
_historyIndex = -1;
_commandController.clear();
return;
}
}
if (_historyIndex >= 0 && _historyIndex < commands.length) {
_commandController.text = commands[_historyIndex]['text'] ?? '';
_commandController.selection = TextSelection.fromPosition(
TextPosition(offset: _commandController.text.length),
);
}
}
void _clearHistory() {
setState(() {
_commandHistory.clear();
_historyIndex = -1;
});
}
String _quickCommandLabel(String key) {
final l10n = context.l10n;
switch (key) {
case 'getName':
return l10n.repeater_cliQuickGetName;
case 'getRadio':
return l10n.repeater_cliQuickGetRadio;
case 'getTx':
return l10n.repeater_cliQuickGetTx;
case 'neighbors':
return l10n.repeater_cliQuickNeighbors;
case 'version':
return l10n.repeater_cliQuickVersion;
case 'advertise':
return l10n.repeater_cliQuickAdvertise;
case 'clock':
return l10n.repeater_cliQuickClock;
case 'clock sync':
return l10n.repeater_cliQuickClockSync;
case 'discovery':
return l10n.repeater_cliQuickDiscovery;
default:
return key;
}
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final scheme = Theme.of(context).colorScheme;
final connector = context.watch<MeshCoreConnector>();
final repeater = _resolveRepeater(connector);
final isFloodMode = repeater.pathOverride == -1;
return Scaffold(
backgroundColor: MeshPalette.bg,
appBar: AppBar(
backgroundColor: MeshPalette.bg1,
title: Text(l10n.repeater_cliTitle),
centerTitle: true,
actions: [
IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onPressed: () =>
ContactRoutingSheet.show(context, contact: repeater),
),
IconButton(
icon: const Icon(Icons.help_outline),
tooltip: l10n.repeater_commandHelp,
onPressed: () => _showCommandHelp(context),
),
IconButton(
icon: const Icon(Icons.clear_all),
tooltip: l10n.repeater_clearHistory,
onPressed: _commandHistory.isEmpty ? null : _clearHistory,
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'debug') {
if (_commandController.text.trim().isNotEmpty) {
_sendCommand(showDebug: true);
} else {
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_enterCommandFirst),
);
}
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'debug',
child: Row(
children: [
const Icon(Icons.bug_report),
const SizedBox(width: 8),
Text(l10n.repeater_debugNextCommand),
],
),
),
],
),
],
),
body: Column(
children: [
// Quick commands bar
Container(
color: MeshPalette.bg1,
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _quickCommands.map((cmd) {
final label = _quickCommandLabel(cmd['labelKey']!);
return Padding(
padding: const EdgeInsets.only(right: 6),
child: ActionChip(
label: Text(
label,
style: MeshTheme.mono(
fontSize: 11,
fontWeight: FontWeight.w600,
color: MeshPalette.blue,
),
),
backgroundColor: MeshPalette.blueBg,
side: const BorderSide(color: MeshPalette.blueLine),
visualDensity: VisualDensity.compact,
onPressed: () => _useQuickCommand(cmd['command']!),
),
);
}).toList(),
),
),
),
Divider(height: 1, color: MeshPalette.line),
// Output area
Expanded(
child: _commandHistory.isEmpty
? _buildEmptyState()
: _buildCommandHistory(),
),
Divider(height: 1, color: MeshPalette.line),
// Command input
Container(
color: MeshPalette.bg1,
padding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
child: SafeArea(
child: Row(
children: [
IconButton(
icon: Icon(
Icons.arrow_upward,
size: 18,
color: scheme.onSurfaceVariant,
),
tooltip: l10n.repeater_previousCommand,
onPressed: () => _navigateHistory(true),
visualDensity: VisualDensity.compact,
),
IconButton(
icon: Icon(
Icons.arrow_downward,
size: 18,
color: scheme.onSurfaceVariant,
),
tooltip: l10n.repeater_nextCommand,
onPressed: () => _navigateHistory(false),
visualDensity: VisualDensity.compact,
),
const SizedBox(width: 4),
Expanded(
child: TextField(
controller: _commandController,
focusNode: _commandFocusNode,
style: MeshTheme.mono(
fontSize: 13,
color: MeshPalette.ink,
),
decoration: InputDecoration(
hintText: context.l10n.repeater_enterCommandHint,
hintStyle: MeshTheme.mono(
fontSize: 13,
color: MeshPalette.ink4,
),
prefixText: '> ',
prefixStyle: MeshTheme.mono(
fontSize: 13,
color: MeshPalette.blue,
fontWeight: FontWeight.w700,
),
filled: true,
fillColor: MeshPalette.bg2,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill),
borderSide: const BorderSide(
color: MeshPalette.line2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill),
borderSide: const BorderSide(
color: MeshPalette.line2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill),
borderSide: const BorderSide(
color: MeshPalette.blue,
width: 1.5,
),
),
),
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendCommand(),
),
),
const SizedBox(width: 6),
Material(
color: MeshPalette.blue.withValues(alpha: 0.15),
shape: const CircleBorder(
side: BorderSide(color: MeshPalette.blueLine),
),
child: InkWell(
customBorder: const CircleBorder(),
onTap: () {
HapticFeedback.lightImpact();
_sendCommand();
},
child: const Padding(
padding: EdgeInsets.all(10),
child: Icon(
Icons.send,
size: 18,
color: MeshPalette.blue,
),
),
),
),
],
),
),
),
],
),
);
}
Widget _buildEmptyState() {
final l10n = context.l10n;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.terminal,
size: 48,
color: MeshPalette.ink4,
),
const SizedBox(height: 12),
Text(
l10n.repeater_noCommandsSent,
style: MeshTheme.mono(
fontSize: 13,
color: MeshPalette.ink3,
),
),
const SizedBox(height: 4),
Text(
l10n.repeater_typeCommandOrUseQuick,
style: const TextStyle(
fontSize: 12,
color: MeshPalette.ink4,
),
),
],
),
);
}
Widget _buildCommandHistory() {
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
itemCount: _commandHistory.length,
itemBuilder: (context, index) {
final entry = _commandHistory[index];
final isCommand = entry['type'] == 'command';
return Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Gutter prefix
SizedBox(
width: 20,
child: Text(
isCommand ? '>' : ' ',
style: MeshTheme.mono(
fontSize: 12,
fontWeight: FontWeight.w700,
color: isCommand
? MeshPalette.blue
: MeshPalette.ink3,
),
),
),
const SizedBox(width: 6),
Expanded(
child: SelectableText(
entry['text']!,
style: MeshTheme.mono(
fontSize: 12.5,
color: isCommand
? MeshPalette.blue
: MeshPalette.ink,
),
),
),
],
),
);
},
);
}
void _applyHelpCommand(String command) {
_commandController.text = command;
_commandController.selection = TextSelection.fromPosition(
TextPosition(offset: command.length),
);
Navigator.pop(context);
Future.microtask(() {
if (mounted) {
_commandFocusNode.requestFocus();
}
});
}
void _showCommandHelp(BuildContext context) {
final l10n = context.l10n;
final generalCommands = [
_CommandHelpEntry(command: 'advert', description: l10n.repeater_cliHelpAdvert),
_CommandHelpEntry(command: 'reboot', description: l10n.repeater_cliHelpReboot),
_CommandHelpEntry(command: 'clock', description: l10n.repeater_cliHelpClock),
_CommandHelpEntry(command: 'password {new-password}', description: l10n.repeater_cliHelpPassword),
_CommandHelpEntry(command: 'ver', description: l10n.repeater_cliHelpVersion),
_CommandHelpEntry(command: 'clear stats', description: l10n.repeater_cliHelpClearStats),
_CommandHelpEntry(command: 'poweroff', description: l10n.repeater_cliHelpPowerOff),
_CommandHelpEntry(command: 'shutdown', description: l10n.repeater_cliHelpPowerOff),
_CommandHelpEntry(command: 'clkreboot', description: l10n.repeater_cliHelpClkReboot),
_CommandHelpEntry(command: 'advert.zerohop', description: l10n.repeater_cliHelpAdvertZeroHop),
_CommandHelpEntry(command: 'start ota', description: l10n.repeater_cliHelpStartOta),
_CommandHelpEntry(command: 'time {epoch-seconds}', description: l10n.repeater_cliHelpTime),
_CommandHelpEntry(command: 'board', description: l10n.repeater_cliHelpBoard),
_CommandHelpEntry(command: 'discover.neighbors', description: l10n.repeater_cliHelpDiscoverNeighbors),
_CommandHelpEntry(command: 'powersaving', description: l10n.repeater_cliHelpPowersaving),
_CommandHelpEntry(command: 'powersaving {on|off}', description: l10n.repeater_cliHelpPowersavingOnOff),
_CommandHelpEntry(command: 'erase', description: l10n.repeater_cliHelpErase),
_CommandHelpEntry(command: 'stats-packets', description: l10n.repeater_cliHelpStatsPackets),
_CommandHelpEntry(command: 'stats-radio', description: l10n.repeater_cliHelpStatsRadio),
_CommandHelpEntry(command: 'stats-core', description: l10n.repeater_cliHelpStatsCore),
];
final settingsCommands = [
_CommandHelpEntry(command: 'set af {air-time-factor}', description: l10n.repeater_cliHelpSetAf),
_CommandHelpEntry(command: 'set tx {tx-power-dbm}', description: l10n.repeater_cliHelpSetTx),
_CommandHelpEntry(command: 'set repeat {on|off}', description: l10n.repeater_cliHelpSetRepeat),
_CommandHelpEntry(command: 'set allow.read.only {on|off}', description: l10n.repeater_cliHelpSetAllowReadOnly),
_CommandHelpEntry(command: 'set flood.max {max-hops}', description: l10n.repeater_cliHelpSetFloodMax),
_CommandHelpEntry(command: 'set int.thresh {db}', description: l10n.repeater_cliHelpSetIntThresh),
_CommandHelpEntry(command: 'set agc.reset.interval {seconds}', description: l10n.repeater_cliHelpSetAgcResetInterval),
_CommandHelpEntry(command: 'set multi.acks {0|1}', description: l10n.repeater_cliHelpSetMultiAcks),
_CommandHelpEntry(command: 'set advert.interval {minutes}', description: l10n.repeater_cliHelpSetAdvertInterval),
_CommandHelpEntry(command: 'set flood.advert.interval {hours}', description: l10n.repeater_cliHelpSetFloodAdvertInterval),
_CommandHelpEntry(command: 'set guest.password {guess-password}', description: l10n.repeater_cliHelpSetGuestPassword),
_CommandHelpEntry(command: 'set name {name}', description: l10n.repeater_cliHelpSetName),
_CommandHelpEntry(command: 'set lat {latitude}', description: l10n.repeater_cliHelpSetLat),
_CommandHelpEntry(command: 'set lon {longitude}', description: l10n.repeater_cliHelpSetLon),
_CommandHelpEntry(command: 'set radio {freq},{bw},{sf},{cr}', description: l10n.repeater_cliHelpSetRadio),
_CommandHelpEntry(command: 'set rxdelay {base}', description: l10n.repeater_cliHelpSetRxDelay),
_CommandHelpEntry(command: 'set txdelay {factor}', description: l10n.repeater_cliHelpSetTxDelay),
_CommandHelpEntry(command: 'set direct.txdelay {factor}', description: l10n.repeater_cliHelpSetDirectTxDelay),
_CommandHelpEntry(command: 'set bridge.enabled {on|off}', description: l10n.repeater_cliHelpSetBridgeEnabled),
_CommandHelpEntry(command: 'set bridge.delay {0-10000}', description: l10n.repeater_cliHelpSetBridgeDelay),
_CommandHelpEntry(command: 'set bridge.source {rx|tx}', description: l10n.repeater_cliHelpSetBridgeSource),
_CommandHelpEntry(command: 'set bridge.baud {speed}', description: l10n.repeater_cliHelpSetBridgeBaud),
_CommandHelpEntry(command: 'set bridge.secret {shared-secret}', description: l10n.repeater_cliHelpSetBridgeSecret),
_CommandHelpEntry(command: 'set adc.multiplier {factor}', description: l10n.repeater_cliHelpSetAdcMultiplier),
_CommandHelpEntry(command: 'tempradio {freq},{bw},{sf},{cr},{minutes}', description: l10n.repeater_cliHelpTempRadio),
_CommandHelpEntry(command: 'setperm {pubkey-hex} {permissions}', description: l10n.repeater_cliHelpSetPerm),
_CommandHelpEntry(command: 'set dutycycle {1-100}', description: l10n.repeater_cliHelpSetDutyCycle),
_CommandHelpEntry(command: 'set prv.key {hex}', description: l10n.repeater_cliHelpSetPrvKey),
_CommandHelpEntry(command: 'set radio.rxgain {on|off}', description: l10n.repeater_cliHelpSetRadioRxGain),
_CommandHelpEntry(command: 'set owner.info {text}', description: l10n.repeater_cliHelpSetOwnerInfo),
_CommandHelpEntry(command: 'set path.hash.mode {0|1|2}', description: l10n.repeater_cliHelpSetPathHashMode),
_CommandHelpEntry(command: 'set loop.detect {off|minimal|moderate|strict}', description: l10n.repeater_cliHelpSetLoopDetect),
_CommandHelpEntry(command: 'set freq {mhz}', description: l10n.repeater_cliHelpSetFreq),
_CommandHelpEntry(command: 'set bridge.channel {1-14}', description: l10n.repeater_cliHelpSetBridgeChannel),
];
final bridgeCommands = [
_CommandHelpEntry(command: 'get bridge.type', description: l10n.repeater_cliHelpGetBridgeType),
];
final loggingCommands = [
_CommandHelpEntry(command: 'log start', description: l10n.repeater_cliHelpLogStart),
_CommandHelpEntry(command: 'log stop', description: l10n.repeater_cliHelpLogStop),
_CommandHelpEntry(command: 'log erase', description: l10n.repeater_cliHelpLogErase),
];
final neighborCommands = [
_CommandHelpEntry(command: 'neighbors', description: l10n.repeater_cliHelpNeighbors),
_CommandHelpEntry(command: 'neighbor.remove {pubkey-prefix}', description: l10n.repeater_cliHelpNeighborRemove),
];
final regionCommands = [
_CommandHelpEntry(command: 'region', description: l10n.repeater_cliHelpRegion),
_CommandHelpEntry(command: 'region load', description: l10n.repeater_cliHelpRegionLoad),
_CommandHelpEntry(command: 'region get {* | name-prefix}', description: l10n.repeater_cliHelpRegionGet),
_CommandHelpEntry(command: 'region put {name} {* | parent-name-prefix}', description: l10n.repeater_cliHelpRegionPut),
_CommandHelpEntry(command: 'region remove {name}', description: l10n.repeater_cliHelpRegionRemove),
_CommandHelpEntry(command: 'region allowf {* | name-prefix}', description: l10n.repeater_cliHelpRegionAllowf),
_CommandHelpEntry(command: 'region denyf {* | name-prefix}', description: l10n.repeater_cliHelpRegionDenyf),
_CommandHelpEntry(command: 'region home', description: l10n.repeater_cliHelpRegionHome),
_CommandHelpEntry(command: 'region home {* | name-prefix}', description: l10n.repeater_cliHelpRegionHomeSet),
_CommandHelpEntry(command: 'region save', description: l10n.repeater_cliHelpRegionSave),
_CommandHelpEntry(command: 'region default', description: l10n.repeater_cliHelpRegionDefault),
_CommandHelpEntry(command: 'region default {* | name-prefix | <null>}', description: l10n.repeater_cliHelpRegionDefaultSet),
_CommandHelpEntry(command: 'region list allowed', description: l10n.repeater_cliHelpRegionListAllowed),
_CommandHelpEntry(command: 'region list denied', description: l10n.repeater_cliHelpRegionListDenied),
];
final getCommands = [
_CommandHelpEntry(command: 'get name', description: l10n.repeater_cliHelpGetName),
_CommandHelpEntry(command: 'get role', description: l10n.repeater_cliHelpGetRole),
_CommandHelpEntry(command: 'get public.key', description: l10n.repeater_cliHelpGetPublicKey),
_CommandHelpEntry(command: 'get prv.key', description: l10n.repeater_cliHelpGetPrvKey),
_CommandHelpEntry(command: 'get repeat', description: l10n.repeater_cliHelpGetRepeat),
_CommandHelpEntry(command: 'get tx', description: l10n.repeater_cliHelpGetTx),
_CommandHelpEntry(command: 'get freq', description: l10n.repeater_cliHelpGetFreq),
_CommandHelpEntry(command: 'get radio', description: l10n.repeater_cliHelpGetRadio),
_CommandHelpEntry(command: 'get radio.rxgain', description: l10n.repeater_cliHelpGetRadioRxGain),
_CommandHelpEntry(command: 'get af', description: l10n.repeater_cliHelpGetAf),
_CommandHelpEntry(command: 'get dutycycle', description: l10n.repeater_cliHelpGetDutyCycle),
_CommandHelpEntry(command: 'get int.thresh', description: l10n.repeater_cliHelpGetIntThresh),
_CommandHelpEntry(command: 'get agc.reset.interval', description: l10n.repeater_cliHelpGetAgcResetInterval),
_CommandHelpEntry(command: 'get multi.acks', description: l10n.repeater_cliHelpGetMultiAcks),
_CommandHelpEntry(command: 'get allow.read.only', description: l10n.repeater_cliHelpGetAllowReadOnly),
_CommandHelpEntry(command: 'get advert.interval', description: l10n.repeater_cliHelpGetAdvertInterval),
_CommandHelpEntry(command: 'get flood.advert.interval', description: l10n.repeater_cliHelpGetFloodAdvertInterval),
_CommandHelpEntry(command: 'get guest.password', description: l10n.repeater_cliHelpGetGuestPassword),
_CommandHelpEntry(command: 'get lat', description: l10n.repeater_cliHelpGetLat),
_CommandHelpEntry(command: 'get lon', description: l10n.repeater_cliHelpGetLon),
_CommandHelpEntry(command: 'get rxdelay', description: l10n.repeater_cliHelpGetRxDelay),
_CommandHelpEntry(command: 'get txdelay', description: l10n.repeater_cliHelpGetTxDelay),
_CommandHelpEntry(command: 'get direct.txdelay', description: l10n.repeater_cliHelpGetDirectTxDelay),
_CommandHelpEntry(command: 'get flood.max', description: l10n.repeater_cliHelpGetFloodMax),
_CommandHelpEntry(command: 'get owner.info', description: l10n.repeater_cliHelpGetOwnerInfo),
_CommandHelpEntry(command: 'get path.hash.mode', description: l10n.repeater_cliHelpGetPathHashMode),
_CommandHelpEntry(command: 'get loop.detect', description: l10n.repeater_cliHelpGetLoopDetect),
_CommandHelpEntry(command: 'get acl', description: l10n.repeater_cliHelpGetAcl),
_CommandHelpEntry(command: 'get bridge.enabled', description: l10n.repeater_cliHelpGetBridgeEnabled),
_CommandHelpEntry(command: 'get bridge.delay', description: l10n.repeater_cliHelpGetBridgeDelay),
_CommandHelpEntry(command: 'get bridge.source', description: l10n.repeater_cliHelpGetBridgeSource),
_CommandHelpEntry(command: 'get bridge.baud', description: l10n.repeater_cliHelpGetBridgeBaud),
_CommandHelpEntry(command: 'get bridge.channel', description: l10n.repeater_cliHelpGetBridgeChannel),
_CommandHelpEntry(command: 'get bridge.secret', description: l10n.repeater_cliHelpGetBridgeSecret),
_CommandHelpEntry(command: 'get bootloader.ver', description: l10n.repeater_cliHelpGetBootloaderVer),
_CommandHelpEntry(command: 'get adc.multiplier', description: l10n.repeater_cliHelpGetAdcMultiplier),
];
final powerMgmtCommands = [
_CommandHelpEntry(command: 'get pwrmgt.support', description: l10n.repeater_cliHelpGetPwrMgtSupport),
_CommandHelpEntry(command: 'get pwrmgt.source', description: l10n.repeater_cliHelpGetPwrMgtSource),
_CommandHelpEntry(command: 'get pwrmgt.bootreason', description: l10n.repeater_cliHelpGetPwrMgtBootReason),
_CommandHelpEntry(command: 'get pwrmgt.bootmv', description: l10n.repeater_cliHelpGetPwrMgtBootMv),
];
final sensorCommands = [
_CommandHelpEntry(command: 'sensor get {key}', description: l10n.repeater_cliHelpSensorGet),
_CommandHelpEntry(command: 'sensor set {key} {value}', description: l10n.repeater_cliHelpSensorSet),
_CommandHelpEntry(command: 'sensor list [start]', description: l10n.repeater_cliHelpSensorList),
];
final gpsCommands = [
_CommandHelpEntry(command: 'gps', description: l10n.repeater_cliHelpGps),
_CommandHelpEntry(command: 'gps {on|off}', description: l10n.repeater_cliHelpGpsOnOff),
_CommandHelpEntry(command: 'gps sync', description: l10n.repeater_cliHelpGpsSync),
_CommandHelpEntry(command: 'gps setloc', description: l10n.repeater_cliHelpGpsSetLoc),
_CommandHelpEntry(command: 'gps advert', description: l10n.repeater_cliHelpGpsAdvert),
_CommandHelpEntry(command: 'gps advert {none|share|prefs}', description: l10n.repeater_cliHelpGpsAdvertSet),
];
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.repeater_commandsListTitle),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.repeater_commandsListNote, style: const TextStyle(fontSize: 13)),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_general, generalCommands),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_getCategory, getCommands),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_settingsCategory, settingsCommands),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_powerMgmt, powerMgmtCommands),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_sensors, sensorCommands),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_bridge, bridgeCommands),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_logging, loggingCommands),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_neighborsRepeaterOnly, neighborCommands),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_regionManagementRepeaterOnly, regionCommands, note: l10n.repeater_regionNote),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_gpsManagement, gpsCommands, note: l10n.repeater_gpsNote),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_close),
),
],
),
);
}
Widget _buildHelpSection(
BuildContext context,
String title,
List<_CommandHelpEntry> commands, {
String? note,
}) {
final scheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
if (note != null) ...[
const SizedBox(height: 4),
Text(note, style: TextStyle(fontSize: 11, color: scheme.onSurfaceVariant)),
],
const SizedBox(height: 8),
...commands.map((entry) => _buildHelpCommandCard(context, entry)),
],
);
}
Widget _buildHelpCommandCard(BuildContext context, _CommandHelpEntry entry) {
final scheme = Theme.of(context).colorScheme;
return Card(
elevation: 0,
margin: const EdgeInsets.only(bottom: 6),
color: scheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(MeshRadii.sm),
side: BorderSide(color: scheme.outlineVariant),
),
child: InkWell(
borderRadius: BorderRadius.circular(MeshRadii.sm),
onTap: () => _applyHelpCommand(entry.command),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.command,
style: MeshTheme.mono(
fontSize: 12,
fontWeight: FontWeight.w600,
color: MeshPalette.blue,
),
),
const SizedBox(height: 4),
Text(
entry.description,
style: TextStyle(fontSize: 12, color: scheme.onSurfaceVariant),
),
],
),
),
),
);
}
}
class _CommandHelpEntry {
final String command;
final String description;
const _CommandHelpEntry({required this.command, required this.description});
}