This commit is contained in:
HDDen
2026-05-09 02:18:00 +03:00
55 changed files with 13669 additions and 1800 deletions
+7 -17
View File
@@ -384,7 +384,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
IconData icon;
Color iconColor;
Color bgColor;
String subtitle;
if (isCommunityChannel) {
// Community channel styling
@@ -392,28 +391,21 @@ class _ChannelsScreenState extends State<ChannelsScreen>
bgColor = Colors.purple.withValues(alpha: 0.2);
if (isCommunityPublic) {
icon = Icons.groups;
subtitle =
'${context.l10n.community_publicChannel}${community.name}';
} else {
icon = Icons.tag;
subtitle =
'${context.l10n.community_hashtagChannel}${community.name}';
}
} else if (channel.isPublicChannel) {
icon = Icons.public;
iconColor = Colors.green;
bgColor = Colors.green.withValues(alpha: 0.2);
subtitle = context.l10n.channels_publicChannel;
} else if (channel.name.startsWith('#')) {
icon = Icons.tag;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
subtitle = context.l10n.channels_hashtagChannel;
} else {
icon = Icons.lock;
iconColor = Colors.blue;
bgColor = Colors.blue.withValues(alpha: 0.2);
subtitle = context.l10n.channels_privateChannel;
}
return Card(
@@ -430,14 +422,17 @@ class _ChannelsScreenState extends State<ChannelsScreen>
: null,
child: ListTile(
dense: true,
minVerticalPadding: 0,
minVerticalPadding: 14,
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
visualDensity: const VisualDensity(vertical: -2),
leading: Stack(
children: [
CircleAvatar(
backgroundColor: bgColor,
child: Icon(icon, color: iconColor),
Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: CircleAvatar(
backgroundColor: bgColor,
child: Icon(icon, color: iconColor),
),
),
if (isCommunityChannel)
Positioned(
@@ -469,11 +464,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
: channel.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
-3
View File
@@ -2011,9 +2011,6 @@ class _MapScreenState extends State<MapScreen> {
color: isPublic ? Colors.orange : Colors.blue,
),
title: Text(label),
subtitle: isPublic
? Text(context.l10n.channels_publicChannel)
: null,
onTap: () async {
Navigator.pop(sheetContext);
final canSend = isPublic
+299
View File
@@ -585,6 +585,62 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
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 = [
@@ -692,6 +748,38 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
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 = [
@@ -768,6 +856,203 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
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 = [
@@ -814,12 +1099,26 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
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(
File diff suppressed because it is too large Load Diff
+25 -7
View File
@@ -38,7 +38,6 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
StreamSubscription<Uint8List>? _frameSubscription;
RepeaterCommandService? _commandService;
Timer? _statusTimeout;
DateTime? _statusRequestedAt;
int? _batteryMv;
int? _uptimeSecs;
int? _queueLen;
@@ -56,6 +55,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
int? _directRx;
int? _dupFlood;
int? _dupDirect;
double? _chanUtil;
PathSelection? _pendingStatusSelection;
@override
@@ -64,7 +64,11 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_setupMessageListener();
_loadStatus();
// Defer until after the first frame so any notifyListeners() triggered
// during preparePathForContactSend doesn't fire mid-build.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _loadStatus();
});
}
@override
@@ -192,6 +196,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_lastSnr = lastSnrRaw / 4.0;
_dupDirect = directDups;
_dupFlood = floodDups;
_chanUtil = ((txAirSecs + rxAirSecs) / uptimeSecs) * 100;
});
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.updateRepeaterBatterySnapshot(
@@ -264,7 +269,6 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
setState(() {
_isLoading = true;
_statusRequestedAt = DateTime.now();
_pendingStatusSelection = null;
_batteryMv = null;
_uptimeSecs = null;
@@ -283,6 +287,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_directRx = null;
_dupFlood = null;
_dupDirect = null;
_chanUtil = null;
});
try {
@@ -570,6 +575,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_buildInfoRow(l10n.repeater_sent, _packetTxText()),
_buildInfoRow(l10n.repeater_received, _packetRxText()),
_buildInfoRow(l10n.repeater_duplicates, _duplicateText()),
_buildInfoRow(l10n.repeater_chanUtil, _chanUtilText()),
],
),
),
@@ -639,11 +645,13 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
}
String _clockText() {
if (_statusRequestedAt == null) return '';
final dt = _statusRequestedAt!;
final date = '${dt.day}/${dt.month}/${dt.year}';
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final dt = connector.repeaterClockAtLogin(widget.repeater.publicKey);
if (dt == null) return '';
final local = dt.toLocal();
final date = '${local.day}/${local.month}/${local.year}';
final time =
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
'${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}';
return '$date $time';
}
@@ -673,6 +681,11 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
return l10n.repeater_packetRxTotal(_packetsRecv!, flood, direct);
}
String _chanUtilText() {
if (_chanUtil == null) return '';
return _formatPercent(_chanUtil);
}
String _duplicateText() {
final l10n = context.l10n;
if (_dupFlood != null || _dupDirect != null) {
@@ -693,6 +706,11 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
return suffix == null ? value.toString() : '$value$suffix';
}
String _formatPercent(double? p) {
if (p == null) return '';
return '${p.toStringAsFixed(2)}%';
}
String _formatSnr(double? snr) {
if (snr == null) return '';
return snr.toStringAsFixed(2);
+14 -2
View File
@@ -305,6 +305,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Icons.chevron_right),
onTap: () => _editLocation(context, connector),
),
if (connector.currentCustomVars?.containsKey('gps') ?? false) ...[
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.gps_fixed),
title: Text(l10n.settings_locationGPSEnable),
subtitle: Text(l10n.settings_locationGPSEnableSubtitle),
value: connector.currentCustomVars?['gps'] == '1',
onChanged: (value) async {
await connector.setCustomVar(value ? 'gps:1' : 'gps:0');
},
),
],
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.group_add_outlined),
@@ -405,8 +417,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
ListTile(
leading: const Icon(Icons.bluetooth_outlined),
title: Text(l10n.settings_bleDebugLog),
subtitle: Text(l10n.settings_bleDebugLogSubtitle),
title: Text(l10n.settings_companionDebugLog),
subtitle: Text(l10n.settings_companionDebugLogSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(