feat(#18): promote Radio Stats to top-level + rename Contact Settings to Contacts

Flatten two more redundant layers in the settings IA. Radio Stats: extract CompanionRadioStatsScreen's body into an embeddable CompanionRadioStatsBody (carrying the 1s-polling acquire/release lifecycle), add a top-level Radio Stats category that renders it directly, and remove the nested Radio Stats tile from the Radio Settings pane (the screen is kept; still pushed from radio_stats_entry.dart). Rename the Contact Settings category to Contacts (reusing contacts_title) with a contacts icon; rename _messagesContactsPane to _contactsPane.

Also fixes a pre-existing chart-freeze surfaced by review: _NoiseChartPainter compared only sample-list length, so the noise chart stopped repainting once the 120-sample buffer filled. Now compares contents via listEquals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/36/head
Strycher 1 month ago
parent 3fb798ca7f
commit 4ba4d22a79

@ -1,18 +1,37 @@
import 'package:flutter/foundation.dart' show listEquals;
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/models/companion_radio_stats.dart';
import 'package:meshcore_open/l10n/l10n.dart';
import 'package:provider/provider.dart';
class CompanionRadioStatsScreen extends StatefulWidget {
class CompanionRadioStatsScreen extends StatelessWidget {
const CompanionRadioStatsScreen({super.key});
@override
State<CompanionRadioStatsScreen> createState() =>
_CompanionRadioStatsScreenState();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.radioStats_screenTitle),
centerTitle: true,
),
body: const CompanionRadioStatsBody(),
);
}
}
/// Embeddable Radio Stats content (no Scaffold) used by both
/// [CompanionRadioStatsScreen] and the settings shell's Radio Stats pane.
/// Acquires 1 s radio-stats polling while mounted and releases it on dispose.
class CompanionRadioStatsBody extends StatefulWidget {
const CompanionRadioStatsBody({super.key});
@override
State<CompanionRadioStatsBody> createState() =>
_CompanionRadioStatsBodyState();
}
class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
class _CompanionRadioStatsBodyState extends State<CompanionRadioStatsBody> {
final List<double> _noiseHistory = [];
static const int _maxSamples = 120;
MeshCoreConnector? _connector;
@ -52,83 +71,71 @@ class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.radioStats_screenTitle),
centerTitle: true,
),
body: Selector<MeshCoreConnector, ({bool connected, bool supported})>(
selector: (_, c) => (
connected: c.isConnected,
supported: c.supportsCompanionRadioStats,
),
builder: (context, state, _) {
if (!state.connected) {
return Center(child: Text(l10n.radioStats_notConnected));
}
if (!state.supported) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
l10n.radioStats_firmwareTooOld,
textAlign: TextAlign.center,
),
return Selector<MeshCoreConnector, ({bool connected, bool supported})>(
selector: (_, c) =>
(connected: c.isConnected, supported: c.supportsCompanionRadioStats),
builder: (context, state, _) {
if (!state.connected) {
return Center(child: Text(l10n.radioStats_notConnected));
}
if (!state.supported) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
l10n.radioStats_firmwareTooOld,
textAlign: TextAlign.center,
),
);
}
final connector = context.read<MeshCoreConnector>();
final scheme = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return ValueListenableBuilder<CompanionRadioStats?>(
valueListenable: connector.radioStatsNotifier,
builder: (context, stats, _) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
if (stats != null) ...[
Text(
l10n.radioStats_noiseFloor(stats.noiseFloorDbm),
style: tt.titleMedium,
),
const SizedBox(height: 4),
Text(l10n.radioStats_lastRssi(stats.lastRssiDbm)),
Text(
l10n.radioStats_lastSnr(
stats.lastSnrDb.toStringAsFixed(1),
),
),
Text(l10n.radioStats_txAir(stats.txAirSecs)),
Text(l10n.radioStats_rxAir(stats.rxAirSecs)),
const SizedBox(height: 16),
] else
Text(l10n.radioStats_waiting),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: CustomPaint(
painter: _NoiseChartPainter(
samples: List<double>.from(_noiseHistory),
colorScheme: scheme,
textTheme: tt,
),
child: const SizedBox.expand(),
),
),
);
}
final connector = context.read<MeshCoreConnector>();
final scheme = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return ValueListenableBuilder<CompanionRadioStats?>(
valueListenable: connector.radioStatsNotifier,
builder: (context, stats, _) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
if (stats != null) ...[
Text(
l10n.radioStats_noiseFloor(stats.noiseFloorDbm),
style: tt.titleMedium,
),
const SizedBox(height: 8),
const SizedBox(height: 4),
Text(l10n.radioStats_lastRssi(stats.lastRssiDbm)),
Text(
l10n.radioStats_chartCaption,
style: tt.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
l10n.radioStats_lastSnr(stats.lastSnrDb.toStringAsFixed(1)),
),
Text(l10n.radioStats_txAir(stats.txAirSecs)),
Text(l10n.radioStats_rxAir(stats.rxAirSecs)),
const SizedBox(height: 16),
] else
Text(l10n.radioStats_waiting),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: CustomPaint(
painter: _NoiseChartPainter(
samples: List<double>.from(_noiseHistory),
colorScheme: scheme,
textTheme: tt,
),
child: const SizedBox.expand(),
),
],
);
},
);
},
),
),
const SizedBox(height: 8),
Text(
l10n.radioStats_chartCaption,
style: tt.bodySmall?.copyWith(color: scheme.onSurfaceVariant),
),
],
);
},
);
},
);
}
}
@ -233,8 +240,8 @@ class _NoiseChartPainter extends CustomPainter {
@override
bool shouldRepaint(covariant _NoiseChartPainter oldDelegate) {
return oldDelegate.samples.length != samples.length ||
oldDelegate.colorScheme != colorScheme;
return oldDelegate.colorScheme != colorScheme ||
!listEquals(oldDelegate.samples, samples);
}
TextPainter _yAxisLabel(double v) {

@ -16,7 +16,7 @@ import 'settings/app_settings_view.dart';
import 'settings/message_settings_view.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
import '../widgets/radio_stats_entry.dart';
import 'companion_radio_stats_screen.dart';
import '../widgets/sync_progress_overlay.dart';
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
@ -78,6 +78,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: l10n.settings_radioSettingsSubtitle,
builder: _radioRangePane,
),
SettingsCategory(
icon: Icons.sensors_outlined,
title: l10n.radioStats_settingsTile,
subtitle: l10n.radioStats_settingsSubtitle,
builder: _radioStatsPane,
),
SettingsCategory(
icon: Icons.badge_outlined,
title: l10n.settings_nodeSettings,
@ -90,10 +96,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
builder: _privacyPane,
),
SettingsCategory(
icon: Icons.forum_outlined,
title: l10n.settings_contactSettings,
icon: Icons.contacts_outlined,
title: l10n.contacts_title,
subtitle: l10n.settings_contactSettingsSubtitle,
builder: _messagesContactsPane,
builder: _contactsPane,
),
SettingsCategory(
icon: Icons.sms_outlined,
@ -144,17 +150,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
const Divider(height: 1),
_PathHashSizeTile(connector: connector),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.sensors_outlined),
title: Text(l10n.radioStats_settingsTile),
subtitle: Text(l10n.radioStats_settingsSubtitle),
trailing: const Icon(Icons.chevron_right),
enabled:
connector.isConnected &&
connector.supportsCompanionRadioStats,
onTap: () => pushCompanionRadioStatsScreen(context),
),
],
),
),
@ -162,6 +157,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Widget _radioStatsPane(BuildContext context) =>
const CompanionRadioStatsBody();
Widget _identityPane(BuildContext context) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
@ -237,7 +235,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Widget _messagesContactsPane(BuildContext context) {
Widget _contactsPane(BuildContext context) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
return ListView(

Loading…
Cancel
Save

Powered by TurnKey Linux.