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:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/models/companion_radio_stats.dart'; import 'package:meshcore_open/models/companion_radio_stats.dart';
import 'package:meshcore_open/l10n/l10n.dart'; import 'package:meshcore_open/l10n/l10n.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class CompanionRadioStatsScreen extends StatefulWidget { class CompanionRadioStatsScreen extends StatelessWidget {
const CompanionRadioStatsScreen({super.key}); const CompanionRadioStatsScreen({super.key});
@override @override
State<CompanionRadioStatsScreen> createState() => Widget build(BuildContext context) {
_CompanionRadioStatsScreenState(); 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 = []; final List<double> _noiseHistory = [];
static const int _maxSamples = 120; static const int _maxSamples = 120;
MeshCoreConnector? _connector; MeshCoreConnector? _connector;
@ -52,83 +71,71 @@ class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return Scaffold( return Selector<MeshCoreConnector, ({bool connected, bool supported})>(
appBar: AppBar( selector: (_, c) =>
title: Text(l10n.radioStats_screenTitle), (connected: c.isConnected, supported: c.supportsCompanionRadioStats),
centerTitle: true, builder: (context, state, _) {
), if (!state.connected) {
body: Selector<MeshCoreConnector, ({bool connected, bool supported})>( return Center(child: Text(l10n.radioStats_notConnected));
selector: (_, c) => ( }
connected: c.isConnected, if (!state.supported) {
supported: c.supportsCompanionRadioStats, return Center(
), child: Padding(
builder: (context, state, _) { padding: const EdgeInsets.all(24),
if (!state.connected) { child: Text(
return Center(child: Text(l10n.radioStats_notConnected)); l10n.radioStats_firmwareTooOld,
} textAlign: TextAlign.center,
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 connector = context.read<MeshCoreConnector>();
final tt = Theme.of(context).textTheme; final scheme = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return ValueListenableBuilder<CompanionRadioStats?>(
valueListenable: connector.radioStatsNotifier, return ValueListenableBuilder<CompanionRadioStats?>(
builder: (context, stats, _) { valueListenable: connector.radioStatsNotifier,
return ListView( builder: (context, stats, _) {
padding: const EdgeInsets.all(16), return ListView(
children: [ padding: const EdgeInsets.all(16),
if (stats != null) ...[ children: [
Text( if (stats != null) ...[
l10n.radioStats_noiseFloor(stats.noiseFloorDbm), Text(
style: tt.titleMedium, 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(),
),
), ),
const SizedBox(height: 8), const SizedBox(height: 4),
Text(l10n.radioStats_lastRssi(stats.lastRssiDbm)),
Text( Text(
l10n.radioStats_chartCaption, l10n.radioStats_lastSnr(stats.lastSnrDb.toStringAsFixed(1)),
style: tt.bodySmall?.copyWith( ),
color: scheme.onSurfaceVariant, 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 @override
bool shouldRepaint(covariant _NoiseChartPainter oldDelegate) { bool shouldRepaint(covariant _NoiseChartPainter oldDelegate) {
return oldDelegate.samples.length != samples.length || return oldDelegate.colorScheme != colorScheme ||
oldDelegate.colorScheme != colorScheme; !listEquals(oldDelegate.samples, samples);
} }
TextPainter _yAxisLabel(double v) { TextPainter _yAxisLabel(double v) {

@ -16,7 +16,7 @@ import 'settings/app_settings_view.dart';
import 'settings/message_settings_view.dart'; import 'settings/message_settings_view.dart';
import 'app_debug_log_screen.dart'; import 'app_debug_log_screen.dart';
import 'ble_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'; import '../widgets/sync_progress_overlay.dart';
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others) /// 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, subtitle: l10n.settings_radioSettingsSubtitle,
builder: _radioRangePane, builder: _radioRangePane,
), ),
SettingsCategory(
icon: Icons.sensors_outlined,
title: l10n.radioStats_settingsTile,
subtitle: l10n.radioStats_settingsSubtitle,
builder: _radioStatsPane,
),
SettingsCategory( SettingsCategory(
icon: Icons.badge_outlined, icon: Icons.badge_outlined,
title: l10n.settings_nodeSettings, title: l10n.settings_nodeSettings,
@ -90,10 +96,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
builder: _privacyPane, builder: _privacyPane,
), ),
SettingsCategory( SettingsCategory(
icon: Icons.forum_outlined, icon: Icons.contacts_outlined,
title: l10n.settings_contactSettings, title: l10n.contacts_title,
subtitle: l10n.settings_contactSettingsSubtitle, subtitle: l10n.settings_contactSettingsSubtitle,
builder: _messagesContactsPane, builder: _contactsPane,
), ),
SettingsCategory( SettingsCategory(
icon: Icons.sms_outlined, icon: Icons.sms_outlined,
@ -144,17 +150,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
const Divider(height: 1), const Divider(height: 1),
_PathHashSizeTile(connector: connector), _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) { Widget _identityPane(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>(); 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 l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>(); final connector = context.watch<MeshCoreConnector>();
return ListView( return ListView(

Loading…
Cancel
Save

Powered by TurnKey Linux.