diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fed6e4ce..2a9fd297 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -324,6 +324,10 @@ "settings_gpsSleepFieldLabel": "Seconds between attempts", "settings_gpsSleepFieldHint": "0 for the firmware default. The radio raises anything under 300 to 300.", "settings_gpsSleepInvalid": "Enter a whole number of seconds, 0 or more.", + "settings_buttonActions": "Button actions", + "settings_buttonActionsSubtitle": "Assign what the button does", + "settings_buzzerTones": "Buzzer and tones", + "settings_buzzerTonesSubtitle": "When this radio beeps", "settings_contactSettings": "Contact Settings", "settings_contactSettingsSubtitle": "Settings for how contacts are added.", "settings_privacyMode": "Privacy Mode", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ea591ba1..119dee78 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1162,6 +1162,30 @@ abstract class AppLocalizations { /// **'Enter a whole number of seconds, 0 or more.'** String get settings_gpsSleepInvalid; + /// No description provided for @settings_buttonActions. + /// + /// In en, this message translates to: + /// **'Button actions'** + String get settings_buttonActions; + + /// No description provided for @settings_buttonActionsSubtitle. + /// + /// In en, this message translates to: + /// **'Assign what the button does'** + String get settings_buttonActionsSubtitle; + + /// No description provided for @settings_buzzerTones. + /// + /// In en, this message translates to: + /// **'Buzzer and tones'** + String get settings_buzzerTones; + + /// No description provided for @settings_buzzerTonesSubtitle. + /// + /// In en, this message translates to: + /// **'When this radio beeps'** + String get settings_buzzerTonesSubtitle; + /// No description provided for @settings_contactSettings. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 99c39637..8391a1e5 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -618,6 +618,18 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Настройки за контакти'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c5aa48ff..3f8727bf 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -620,6 +620,18 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Kontakteinstellungen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 642081d8..fbe4226b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -614,6 +614,18 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Contact Settings'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 0f82d33b..9c08b90e 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -618,6 +618,18 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Configuración de contacto'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 28722c79..466a85f0 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -621,6 +621,18 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Paramètres de contact'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 1a4454b4..a8c9d72e 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -620,6 +620,18 @@ class AppLocalizationsHu extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Kapcsolat beállítások'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 84642200..776a766f 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -620,6 +620,18 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Impostazioni di contatto'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 4b8f354a..36e8383c 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -603,6 +603,18 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => '連絡設定'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 4ba06ec3..adec5408 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -603,6 +603,18 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => '연락처 설정'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 355cc7cc..3624ba23 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -617,6 +617,18 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Contactinstellingen'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index c7a791aa..54678f8c 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -623,6 +623,18 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Ustawienia kontaktów'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index d7b2cd60..dfa50e00 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -620,6 +620,18 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Configurações de Contato'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 1efe862d..2b5d4c9f 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -619,6 +619,18 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Настройки контактов'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 9d4ea4a3..41bf9d82 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -618,6 +618,18 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Nastavenia kontaktov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 5e38d5f5..f6288d99 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -616,6 +616,18 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Nastavitve stika'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 86cc5b18..41e3dbb1 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -615,6 +615,18 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Kontaktinställningar'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 3a0b84b7..79efce02 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -617,6 +617,18 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => 'Налаштування контактів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 4fd4e4b1..6c89bd3b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -599,6 +599,18 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_gpsSleepInvalid => 'Enter a whole number of seconds, 0 or more.'; + @override + String get settings_buttonActions => 'Button actions'; + + @override + String get settings_buttonActionsSubtitle => 'Assign what the button does'; + + @override + String get settings_buzzerTones => 'Buzzer and tones'; + + @override + String get settings_buzzerTonesSubtitle => 'When this radio beeps'; + @override String get settings_contactSettings => '联系人设置'; diff --git a/lib/screens/settings/device_ui_view.dart b/lib/screens/settings/device_ui_view.dart index 95f56190..e9a6f40c 100644 --- a/lib/screens/settings/device_ui_view.dart +++ b/lib/screens/settings/device_ui_view.dart @@ -13,13 +13,22 @@ import '../../connector/offband_device_ui.dart'; /// /// English-only for now, matching the serial-capture pane (#430); localization /// is a follow-up rather than a blocker on shipping the capability. +/// +/// Shows one [part] at a time (#978): button actions open from Node Settings, +/// the buzzer from Message Settings. They are separate features with separate +/// bits, and boards with a buzzer and no button exist. class DeviceUiView extends StatefulWidget { - const DeviceUiView({super.key}); + const DeviceUiView({super.key, required this.part}); + + final DeviceUiPart part; @override State createState() => _DeviceUiViewState(); } +/// Which half of the device UI a [DeviceUiView] shows. +enum DeviceUiPart { buttons, buzzer } + class _DeviceUiViewState extends State { @override void initState() { @@ -29,17 +38,34 @@ class _DeviceUiViewState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final c = context.read(); - c.requestButtonMatrix(); - c.requestNotifyScope(); + switch (widget.part) { + case DeviceUiPart.buttons: + c.requestButtonMatrix(); + case DeviceUiPart.buzzer: + c.requestNotifyScope(); + } }); } + /// An error is shown on the screen it belongs to. One that could not be + /// attributed to a write is shown on both halves rather than hidden. + bool _ownsError(DeviceUiSurface? surface) => switch (surface) { + null => true, + DeviceUiSurface.buttonMatrix => widget.part == DeviceUiPart.buttons, + DeviceUiSurface.notifyScope || + DeviceUiSurface.channelTune => widget.part == DeviceUiPart.buzzer, + DeviceUiSurface.gpsMode => false, + }; + @override Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { - final showButtons = connector.supportsButtonMatrix; - final showScope = connector.supportsNotifyScope; + final showButtons = + widget.part == DeviceUiPart.buttons && + connector.supportsButtonMatrix; + final showScope = + widget.part == DeviceUiPart.buzzer && connector.supportsNotifyScope; // A radio advertising neither bit has no button and no buzzer to // configure, so it gets nothing. The tile that leads here is gated the // same way, so this is a belt-and-braces guard rather than a path a @@ -82,7 +108,8 @@ class _DeviceUiViewState extends State { } return ListView( children: [ - if (connector.deviceUiError != null) + if (connector.deviceUiError != null && + _ownsError(connector.deviceUiErrorSurface)) _ErrorBanner( message: connector.deviceUiError!, onDismiss: connector.clearDeviceUiError, @@ -98,7 +125,6 @@ class _DeviceUiViewState extends State { ), ), ..._scopeTiles(connector), - const Divider(height: 24), ], if (showButtons) ...[ const _SectionHeader('Button actions'), diff --git a/lib/screens/settings/message_settings_view.dart b/lib/screens/settings/message_settings_view.dart index 151cc602..a81f615f 100644 --- a/lib/screens/settings/message_settings_view.dart +++ b/lib/screens/settings/message_settings_view.dart @@ -1,16 +1,19 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../connector/meshcore_connector.dart'; import '../../l10n/l10n.dart'; import '../../services/app_settings_service.dart'; import '../../services/notification_service.dart'; import '../../helpers/settings_persist.dart'; import '../../helpers/snack_bar_builder.dart'; +import 'device_ui_view.dart'; /// Embeddable view (no Scaffold) for the Message Settings shell pane. /// -/// Renders notification toggles and message-handling settings. Both cards read -/// and write only [AppSettingsService]. +/// Renders notification toggles and message-handling settings, which read and +/// write only [AppSettingsService]. Between them, on a radio with a buzzer, +/// sits the entry to the radio's own buzzer settings (#978). class MessageSettingsView extends StatelessWidget { const MessageSettingsView({super.key}); @@ -23,6 +26,7 @@ class MessageSettingsView extends StatelessWidget { children: [ _buildNotificationsCard(context, settingsService), const SizedBox(height: 16), + const _RadioBuzzerEntry(), _buildMessagingCard(context, settingsService), ], ); @@ -404,3 +408,40 @@ class MessageSettingsView extends StatelessWidget { ); } } + +/// Entry to the radio's buzzer settings (#978), shown only where the radio +/// advertises a buzzer (caps byte 2 `0x01`). Separate from the card above, +/// which governs whether this PHONE notifies; this governs the radio. +class _RadioBuzzerEntry extends StatelessWidget { + const _RadioBuzzerEntry(); + + @override + Widget build(BuildContext context) { + final connector = context.watch(); + if (!connector.supportsNotifyScope) return const SizedBox.shrink(); + final l10n = context.l10n; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Card( + child: ListTile( + leading: const Icon(Icons.volume_up_outlined), + title: Text(l10n.settings_buzzerTones), + subtitle: Text(l10n.settings_buzzerTonesSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + appBar: AppBar( + title: Text(l10n.settings_buzzerTones), + centerTitle: true, + ), + body: const DeviceUiView(part: DeviceUiPart.buzzer), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 7628f7da..302d2304 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -430,23 +430,21 @@ class _SettingsScreenState extends State { onTap: () => _editNodeName(context, connector), ), // Location and the GPS toggle moved to their own category (#977). - // Button and buzzer: device UI config (#474/#475). Owner-placed - // under Node Settings, directly above the public key. + // Button actions (#474), directly above the public key. The + // buzzer moved to Message Settings (#978): boards with a buzzer + // and no button exist, so the two are separate entries on + // separate bits. // - // Gated STRICTLY on the capability bits. A radio without the - // hardware has no button to configure, so it gets no tile, no - // screen and no command. This is the negative test in #474: - // "A device that does not advertise the bit shows no screen and - // the client emits no command." - if (connector.supportsButtonMatrix || - connector.supportsNotifyScope) ...[ + // Gated STRICTLY on the capability bit. A radio without a button + // gets no tile, no screen and no command. This is the negative + // test in #474: "A device that does not advertise the bit shows + // no screen and the client emits no command." + if (connector.supportsButtonMatrix) ...[ const Divider(height: 1), ListTile( leading: const Icon(Icons.radio_button_checked_outlined), - title: const Text('Button and buzzer'), - subtitle: const Text( - 'Assign button actions and set when this radio beeps', - ), + title: Text(l10n.settings_buttonActions), + subtitle: Text(l10n.settings_buttonActionsSubtitle), trailing: const Icon(Icons.chevron_right), onTap: () { Navigator.push( @@ -454,10 +452,10 @@ class _SettingsScreenState extends State { MaterialPageRoute( builder: (context) => Scaffold( appBar: AppBar( - title: const Text('Button and buzzer'), + title: Text(l10n.settings_buttonActions), centerTitle: true, ), - body: const DeviceUiView(), + body: const DeviceUiView(part: DeviceUiPart.buttons), ), ), ); diff --git a/test/screens/settings_device_ui_split_test.dart b/test/screens/settings_device_ui_split_test.dart new file mode 100644 index 00000000..4b6127a8 --- /dev/null +++ b/test/screens/settings_device_ui_split_test.dart @@ -0,0 +1,161 @@ +// Pins #978: button actions live under Node Settings on caps byte 2 0x02, the +// buzzer under Message Settings on 0x01, and each opens only its own half. +// All four combinations are covered, because boards with a buzzer and no +// button exist (m5stack_unit_c6l, thinknode_m9, wio-tracker-l1-eink). + +import 'package:drift/native.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/connector/offband_device_ui.dart'; +import 'package:meshcore_open/l10n/app_localizations.dart'; +import 'package:meshcore_open/screens/settings_screen.dart'; +import 'package:meshcore_open/services/app_settings_service.dart'; +import 'package:meshcore_open/storage/drift/blob_store.dart'; +import 'package:meshcore_open/storage/drift/offband_database.dart'; +import 'package:meshcore_open/storage/prefs_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class _Radio extends MeshCoreConnector { + _Radio({required this.button, required this.buzzer}); + final bool button; + final bool buzzer; + final List requests = []; + + @override + bool get supportsButtonMatrix => button; + + @override + bool get supportsNotifyScope => buzzer; + + // Values already read, so no screen sits on an animating spinner. + @override + ButtonMatrix? get buttonMatrix => + const ButtonMatrix(assignments: {}, supportedActions: 0); + + @override + DeviceNotifyScope? get deviceNotifyScope => DeviceNotifyScope.all; + + @override + Future requestButtonMatrix() async => requests.add('matrix'); + + @override + Future requestNotifyScope() async => requests.add('scope'); +} + +void main() { + late OffbandDatabase db; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + PrefsManager.reset(); + await PrefsManager.initialize(); + db = OffbandDatabase(NativeDatabase.memory()); + BlobStore.overrideForTest(BlobStore(db)); + }); + + tearDown(() async { + BlobStore.clearTestOverride(); + await db.close(); + }); + + Future pumpSettings(WidgetTester tester, _Radio radio) async { + radio.sendFrameOverrideForTest = (_) {}; + tester.view.physicalSize = const Size(400, 1000); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: radio), + ChangeNotifierProvider( + create: (_) => AppSettingsService(), + ), + ], + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: SettingsScreen(), + ), + ), + ); + await tester.pumpAndSettle(); + } + + Future tapText(WidgetTester tester, String text) async { + await tester.scrollUntilVisible(find.text(text), 200); + await tester.tap(find.text(text)); + await tester.pumpAndSettle(); + } + + Future back(WidgetTester tester) async { + await tester.pageBack(); + await tester.pumpAndSettle(); + } + + /// What Node Settings and Message Settings each offer for this radio. + Future<(bool, bool)> entries(WidgetTester tester, _Radio radio) async { + await pumpSettings(tester, radio); + await tapText(tester, 'Node Settings'); + final hasButton = find.text('Button actions').evaluate().isNotEmpty; + await back(tester); + await tapText(tester, 'Message Settings'); + final hasBuzzer = find.text('Buzzer and tones').evaluate().isNotEmpty; + return (hasButton, hasBuzzer); + } + + testWidgets('button only: an entry in Node Settings, none for a buzzer', ( + tester, + ) async { + final r = await entries(tester, _Radio(button: true, buzzer: false)); + expect(r, equals((true, false))); + }); + + testWidgets('buzzer only: an entry in Message Settings, none for a button', ( + tester, + ) async { + final r = await entries(tester, _Radio(button: false, buzzer: true)); + expect(r, equals((false, true))); + }); + + testWidgets('both: each in its own place', (tester) async { + final r = await entries(tester, _Radio(button: true, buzzer: true)); + expect(r, equals((true, true))); + }); + + testWidgets('neither: no entry anywhere', (tester) async { + final r = await entries(tester, _Radio(button: false, buzzer: false)); + expect(r, equals((false, false))); + }); + + testWidgets('the combined Button and buzzer entry is gone', (tester) async { + await pumpSettings(tester, _Radio(button: true, buzzer: true)); + await tapText(tester, 'Node Settings'); + expect(find.text('Button and buzzer'), findsNothing); + }); + + testWidgets('Buzzer and tones opens only the buzzer half', (tester) async { + final radio = _Radio(button: true, buzzer: true); + await pumpSettings(tester, radio); + await tapText(tester, 'Message Settings'); + await tapText(tester, 'Buzzer and tones'); + + expect(find.text('Device notification scope'), findsOneWidget); + // A button row: present whenever the button half renders. + expect(find.text('Single press'), findsNothing); + expect(radio.requests, equals(['scope'])); + }); + + testWidgets('Button actions opens only the button half', (tester) async { + final radio = _Radio(button: true, buzzer: true); + await pumpSettings(tester, radio); + await tapText(tester, 'Node Settings'); + await tapText(tester, 'Button actions'); + + expect(find.text('Single press'), findsOneWidget); + expect(find.text('Device notification scope'), findsNothing); + expect(radio.requests, equals(['matrix'])); + }); +} diff --git a/untranslated.json b/untranslated.json index 29b98bf3..6a12ec1f 100644 --- a/untranslated.json +++ b/untranslated.json @@ -59,6 +59,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -369,6 +373,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -679,6 +687,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -989,6 +1001,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -1299,6 +1315,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -1609,6 +1629,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -1919,6 +1943,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -2229,6 +2257,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -2539,6 +2571,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -2849,6 +2885,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -3159,6 +3199,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -3469,6 +3513,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -3779,6 +3827,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -4089,6 +4141,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -4399,6 +4455,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -4709,6 +4769,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -5019,6 +5083,10 @@ "settings_gpsSleepFieldLabel", "settings_gpsSleepFieldHint", "settings_gpsSleepInvalid", + "settings_buttonActions", + "settings_buttonActionsSubtitle", + "settings_buzzerTones", + "settings_buzzerTonesSubtitle", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription",