From 4ba4d22a7941bbe4c788029d93a64e82dbcea257 Mon Sep 17 00:00:00 2001 From: Strycher Date: Sun, 14 Jun 2026 18:04:10 -0400 Subject: [PATCH] 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 --- lib/screens/companion_radio_stats_screen.dart | 163 +++++++++--------- lib/screens/settings_screen.dart | 30 ++-- 2 files changed, 99 insertions(+), 94 deletions(-) diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart index 9c37676..b17bd8b 100644 --- a/lib/screens/companion_radio_stats_screen.dart +++ b/lib/screens/companion_radio_stats_screen.dart @@ -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 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 createState() => + _CompanionRadioStatsBodyState(); } -class _CompanionRadioStatsScreenState extends State { +class _CompanionRadioStatsBodyState extends State { final List _noiseHistory = []; static const int _maxSamples = 120; MeshCoreConnector? _connector; @@ -52,83 +71,71 @@ class _CompanionRadioStatsScreenState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; - return Scaffold( - appBar: AppBar( - title: Text(l10n.radioStats_screenTitle), - centerTitle: true, - ), - body: Selector( - 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( + 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(); - final scheme = Theme.of(context).colorScheme; - final tt = Theme.of(context).textTheme; - - return ValueListenableBuilder( - 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.from(_noiseHistory), - colorScheme: scheme, - textTheme: tt, - ), - child: const SizedBox.expand(), - ), + ), + ); + } + final connector = context.read(); + final scheme = Theme.of(context).colorScheme; + final tt = Theme.of(context).textTheme; + + return ValueListenableBuilder( + 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.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) { diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 3109913..476a361 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -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 { 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 { 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 { ), 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 { ); } + Widget _radioStatsPane(BuildContext context) => + const CompanionRadioStatsBody(); + Widget _identityPane(BuildContext context) { final l10n = context.l10n; final connector = context.watch(); @@ -237,7 +235,7 @@ class _SettingsScreenState extends State { ); } - Widget _messagesContactsPane(BuildContext context) { + Widget _contactsPane(BuildContext context) { final l10n = context.l10n; final connector = context.watch(); return ListView(