From d8cb35c5950f2de7a5e819259982f6d3949d5991 Mon Sep 17 00:00:00 2001 From: Janez T Date: Thu, 19 Mar 2026 14:18:29 +0100 Subject: [PATCH] fix: Simplify device settings styling #123 --- lib/screens/contacts_tab.dart | 83 +--- lib/screens/device_config_screen.dart | 190 ++++----- lib/widgets/contacts/contact_tile.dart | 9 +- lib/widgets/device/spectrum_scan_panel.dart | 26 +- .../messages/recipient_selector_sheet.dart | 376 +++++++----------- test/screens/contacts_tab_test.dart | 40 +- 6 files changed, 272 insertions(+), 452 deletions(-) diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index 179dfbb..e4df9e4 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -934,39 +934,6 @@ class _ContactsTabState extends State { context, ContactSection.repeaters, ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (connectionProvider.deviceInfo.isConnected) - IconButton( - icon: const Icon(Icons.radar, size: 20), - tooltip: 'Discover repeaters', - visualDensity: VisualDensity.compact, - style: IconButton.styleFrom( - foregroundColor: _sectionAccentColor( - context, - ContactSection.repeaters, - ), - backgroundColor: _sectionAccentColor( - context, - ContactSection.repeaters, - ).withValues(alpha: 0.10), - ), - onPressed: () { - context - .read() - .discoverNodeType(advertType: 2); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - l10n.repeaterDiscoverySent, - ), - ), - ); - }, - ), - ], - ), ), _buildSectionFilterField( context, @@ -1029,37 +996,6 @@ class _ContactsTabState extends State { context, ContactSection.sensors, ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (connectionProvider.deviceInfo.isConnected) - IconButton( - icon: const Icon(Icons.radar, size: 20), - tooltip: 'Discover sensors', - visualDensity: VisualDensity.compact, - style: IconButton.styleFrom( - foregroundColor: _sectionAccentColor( - context, - ContactSection.sensors, - ), - backgroundColor: _sectionAccentColor( - context, - ContactSection.sensors, - ).withValues(alpha: 0.10), - ), - onPressed: () { - context - .read() - .discoverNodeType(advertType: 4); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.sensorDiscoverySent), - ), - ); - }, - ), - ], - ), ), _buildSectionFilterField( context, @@ -1718,15 +1654,7 @@ class _SectionCard extends StatelessWidget { margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(14), decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.surface, - accentColor.withValues(alpha: 0.04), - colorScheme.surfaceContainerLow, - ], - ), + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(26), border: Border.all(color: accentColor.withValues(alpha: 0.14)), boxShadow: [ @@ -1903,14 +1831,7 @@ class _ChannelActivityCard extends StatelessWidget { margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(18), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.surfaceContainerLow, - colorScheme.surfaceContainerHighest.withValues(alpha: 0.9), - ], - ), + color: colorScheme.surfaceContainerLow, border: Border.all( color: colorScheme.outlineVariant.withValues(alpha: 0.35), ), diff --git a/lib/screens/device_config_screen.dart b/lib/screens/device_config_screen.dart index 99eef9a..b8492de 100644 --- a/lib/screens/device_config_screen.dart +++ b/lib/screens/device_config_screen.dart @@ -729,9 +729,9 @@ class _DeviceConfigScreenState extends State { } catch (e) { if (!mounted) return; setState(() => _gpsLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to set GPS mode: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to set GPS mode: $e'))); } } @@ -893,7 +893,9 @@ class _DeviceConfigScreenState extends State { .toList(); if (contacts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context)!.noDeviceContactsToClear)), + SnackBar( + content: Text(AppLocalizations.of(context)!.noDeviceContactsToClear), + ), ); return; } @@ -988,7 +990,9 @@ class _DeviceConfigScreenState extends State { .toList(); if (channels.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context)!.noCustomChannelsToClear)), + SnackBar( + content: Text(AppLocalizations.of(context)!.noCustomChannelsToClear), + ), ); return; } @@ -1095,19 +1099,8 @@ class _DeviceConfigScreenState extends State { return Scaffold( appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)), - body: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - colorScheme.primaryContainer.withValues(alpha: 0.55), - colorScheme.surface, - colorScheme.surface, - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: const [0.0, 0.22, 1.0], - ), - ), + body: ColoredBox( + color: colorScheme.surface, child: SafeArea( child: ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), @@ -1146,7 +1139,9 @@ class _DeviceConfigScreenState extends State { SizedBox(height: 20), _ConfigSectionCard( title: AppLocalizations.of(context)!.storage, - subtitle: AppLocalizations.of(context)!.availableSpaceOnThisDevice, + subtitle: AppLocalizations.of( + context, + )!.availableSpaceOnThisDevice, icon: Icons.storage_rounded, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1190,7 +1185,9 @@ class _DeviceConfigScreenState extends State { icon: _autoAddDiscoveredContactsEnabled ? Icons.person_add_alt_1 : Icons.person_add_disabled, - title: AppLocalizations.of(context)!.enableAutomaticAdding, + title: AppLocalizations.of( + context, + )!.enableAutomaticAdding, description: 'Turn this off to keep discoveries manual-only on the radio.', accentColor: _autoAddDiscoveredContactsEnabled @@ -1293,7 +1290,9 @@ class _DeviceConfigScreenState extends State { SizedBox(height: 14), _SettingHighlightCard( icon: Icons.history_toggle_off_rounded, - title: AppLocalizations.of(context)!.overwriteOldestWhenFull, + title: AppLocalizations.of( + context, + )!.overwriteOldestWhenFull, description: 'Allow the radio to replace the oldest contact when storage is full.', accentColor: _overwriteOldestAutoAddEnabled @@ -1329,7 +1328,9 @@ class _DeviceConfigScreenState extends State { : _saveAutoDiscoverySettings, isSaving: _isSavingAutoDiscoverySettings, isSaved: _autoDiscoverySettingsSaved, - label: AppLocalizations.of(context)!.saveDiscoverySettings, + label: AppLocalizations.of( + context, + )!.saveDiscoverySettings, ), ), ], @@ -1338,7 +1339,9 @@ class _DeviceConfigScreenState extends State { SizedBox(height: 20), _ConfigSectionCard( title: AppLocalizations.of(context)!.publicInfo, - subtitle: AppLocalizations.of(context)!.chooseTheNameAndLocationThisDeviceShares, + subtitle: AppLocalizations.of( + context, + )!.chooseTheNameAndLocationThisDeviceShares, icon: Icons.public_rounded, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1368,9 +1371,7 @@ class _DeviceConfigScreenState extends State { if (_gpsEnabled != null) ...[ const SizedBox(height: 12), _SettingHighlightCard( - icon: _gpsEnabled! - ? Icons.gps_fixed - : Icons.gps_off, + icon: _gpsEnabled! ? Icons.gps_fixed : Icons.gps_off, title: AppLocalizations.of(context)!.gpsModule, description: 'Enable or disable the onboard GPS hardware.', @@ -1411,13 +1412,9 @@ class _DeviceConfigScreenState extends State { Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: colorScheme.surface.withValues(alpha: 0.65), + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(22), - border: Border.all( - color: colorScheme.outlineVariant.withValues( - alpha: 0.7, - ), - ), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1492,7 +1489,9 @@ class _DeviceConfigScreenState extends State { SizedBox(height: 20), _ConfigSectionCard( title: AppLocalizations.of(context)!.radioSettings, - subtitle: AppLocalizations.of(context)!.chooseAPresetOrFinetuneCustomRadioSettings, + subtitle: AppLocalizations.of( + context, + )!.chooseAPresetOrFinetuneCustomRadioSettings, icon: Icons.settings_input_antenna_rounded, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1716,7 +1715,9 @@ class _DeviceConfigScreenState extends State { SizedBox(height: 16), _SettingHighlightCard( icon: Icons.repeat_rounded, - title: AppLocalizations.of(context)!.repeatNearbyTraffic, + title: AppLocalizations.of( + context, + )!.repeatNearbyTraffic, description: deviceInfo.allowedRepeatFreqRanges != null && deviceInfo.allowedRepeatFreqRanges!.isNotEmpty @@ -1762,7 +1763,9 @@ class _DeviceConfigScreenState extends State { SizedBox(height: 20), _ConfigSectionCard( title: AppLocalizations.of(context)!.dangerZone, - subtitle: AppLocalizations.of(context)!.destructiveDeviceActions, + subtitle: AppLocalizations.of( + context, + )!.destructiveDeviceActions, icon: Icons.warning_amber_rounded, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1770,13 +1773,9 @@ class _DeviceConfigScreenState extends State { Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: colorScheme.errorContainer.withValues( - alpha: 0.55, - ), + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(22), - border: Border.all( - color: colorScheme.error.withValues(alpha: 0.28), - ), + border: Border.all(color: colorScheme.error), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -1794,14 +1793,14 @@ class _DeviceConfigScreenState extends State { 'Wipe data on device', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w800, - color: colorScheme.onErrorContainer, + color: colorScheme.onSurface, ), ), const SizedBox(height: 4), Text( 'Erase contacts, keys, and radio settings from the connected MeshCore device and return it to factory defaults.', style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onErrorContainer, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -1831,7 +1830,9 @@ class _DeviceConfigScreenState extends State { ), ) : Icon(Icons.people_alt_outlined), - label: Text(AppLocalizations.of(context)!.clearAllContacts), + label: Text( + AppLocalizations.of(context)!.clearAllContacts, + ), ), ), const SizedBox(height: 12), @@ -1855,7 +1856,9 @@ class _DeviceConfigScreenState extends State { ), ) : Icon(Icons.forum_outlined), - label: Text(AppLocalizations.of(context)!.clearAllChannels), + label: Text( + AppLocalizations.of(context)!.clearAllChannels, + ), ), ), const SizedBox(height: 12), @@ -1871,7 +1874,9 @@ class _DeviceConfigScreenState extends State { minimumSize: const Size.fromHeight(52), ), icon: Icon(Icons.delete_forever_rounded), - label: Text(AppLocalizations.of(context)!.wipeDeviceData), + label: Text( + AppLocalizations.of(context)!.wipeDeviceData, + ), ), ), ], @@ -1930,26 +1935,9 @@ class _ConfigHeroCard extends StatelessWidget { return Container( padding: const EdgeInsets.all(22), decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - colorScheme.primaryContainer, - colorScheme.primary.withValues(alpha: 0.14), - colorScheme.surfaceContainerHighest, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(28), - border: Border.all( - color: colorScheme.onPrimaryContainer.withValues(alpha: 0.08), - ), - boxShadow: [ - BoxShadow( - color: colorScheme.primary.withValues(alpha: 0.10), - blurRadius: 26, - offset: const Offset(0, 12), - ), - ], + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1960,13 +1948,10 @@ class _ConfigHeroCard extends StatelessWidget { width: 56, height: 56, decoration: BoxDecoration( - color: colorScheme.onPrimaryContainer.withValues(alpha: 0.12), + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(18), ), - child: Icon( - Icons.tune_rounded, - color: colorScheme.onPrimaryContainer, - ), + child: Icon(Icons.tune_rounded, color: colorScheme.primary), ), const SizedBox(width: 14), Expanded( @@ -1979,15 +1964,13 @@ class _ConfigHeroCard extends StatelessWidget { vertical: 6, ), decoration: BoxDecoration( - color: colorScheme.onPrimaryContainer.withValues( - alpha: 0.10, - ), + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(999), ), child: Text( 'Device settings', style: theme.textTheme.labelMedium?.copyWith( - color: colorScheme.onPrimaryContainer, + color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w700, ), ), @@ -1997,16 +1980,14 @@ class _ConfigHeroCard extends StatelessWidget { title, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w800, - color: colorScheme.onPrimaryContainer, + color: colorScheme.onSurface, ), ), const SizedBox(height: 4), Text( subtitle, style: theme.textTheme.bodyMedium?.copyWith( - color: colorScheme.onPrimaryContainer.withValues( - alpha: 0.82, - ), + color: colorScheme.onSurfaceVariant, height: 1.35, ), ), @@ -2050,23 +2031,24 @@ class _HeroStat extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; - final background = data.emphasized - ? colorScheme.primary.withValues(alpha: 0.18) - : colorScheme.onPrimaryContainer.withValues(alpha: 0.10); + final iconColor = data.emphasized + ? colorScheme.primary + : colorScheme.onSurfaceVariant; + final borderColor = data.emphasized + ? colorScheme.primary + : colorScheme.outlineVariant; return Container( constraints: const BoxConstraints(minWidth: 140), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( - color: background, + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20), - border: Border.all( - color: colorScheme.onPrimaryContainer.withValues(alpha: 0.08), - ), + border: Border.all(color: borderColor), ), child: Row( children: [ - Icon(data.icon, size: 18, color: colorScheme.onPrimaryContainer), + Icon(data.icon, size: 18, color: iconColor), const SizedBox(width: 10), Expanded( child: Column( @@ -2075,9 +2057,7 @@ class _HeroStat extends StatelessWidget { Text( data.label, style: theme.textTheme.labelMedium?.copyWith( - color: colorScheme.onPrimaryContainer.withValues( - alpha: 0.76, - ), + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 2), @@ -2086,7 +2066,7 @@ class _HeroStat extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleSmall?.copyWith( - color: colorScheme.onPrimaryContainer, + color: colorScheme.onSurface, fontWeight: FontWeight.w800, ), ), @@ -2118,7 +2098,9 @@ class _ConfigSectionCard extends StatelessWidget { final colorScheme = theme.colorScheme; return Card( + color: colorScheme.surfaceContainerLow, elevation: 0, + surfaceTintColor: Colors.transparent, clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), child: Padding( @@ -2133,7 +2115,7 @@ class _ConfigSectionCard extends StatelessWidget { width: 42, height: 42, decoration: BoxDecoration( - color: colorScheme.primary.withValues(alpha: 0.10), + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(14), ), child: Icon(icon, color: colorScheme.primary), @@ -2259,14 +2241,7 @@ class _StorageUsageMeter extends StatelessWidget { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - colorScheme.surfaceContainerHighest.withValues(alpha: 0.65), - colorScheme.surfaceContainer, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(18), border: Border.all(color: colorScheme.outlineVariant), ), @@ -2321,9 +2296,9 @@ class _SelectedPresetCard extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: colorScheme.primaryContainer.withValues(alpha: 0.55), + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(20), - border: Border.all(color: colorScheme.primary.withValues(alpha: 0.18)), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -2418,16 +2393,9 @@ class _SettingHighlightCard extends StatelessWidget { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - accentColor.withValues(alpha: 0.14), - colorScheme.surfaceContainerHigh.withValues(alpha: 0.85), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(22), - border: Border.all(color: accentColor.withValues(alpha: 0.18)), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -2436,7 +2404,7 @@ class _SettingHighlightCard extends StatelessWidget { width: 42, height: 42, decoration: BoxDecoration( - color: accentColor.withValues(alpha: 0.16), + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(14), ), child: Icon(icon, color: accentColor), diff --git a/lib/widgets/contacts/contact_tile.dart b/lib/widgets/contacts/contact_tile.dart index e016555..46993ca 100644 --- a/lib/widgets/contacts/contact_tile.dart +++ b/lib/widgets/contacts/contact_tile.dart @@ -185,14 +185,7 @@ class ContactTile extends StatelessWidget { margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(18), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.surfaceContainerLow, - colorScheme.surfaceContainerHighest.withValues(alpha: 0.9), - ], - ), + color: colorScheme.surfaceContainerLow, border: Border.all( color: colorScheme.outlineVariant.withValues(alpha: 0.35), ), diff --git a/lib/widgets/device/spectrum_scan_panel.dart b/lib/widgets/device/spectrum_scan_panel.dart index 4c3580a..e232413 100644 --- a/lib/widgets/device/spectrum_scan_panel.dart +++ b/lib/widgets/device/spectrum_scan_panel.dart @@ -106,7 +106,10 @@ class SpectrumScanPanel extends StatelessWidget { color: theme.colorScheme.primary, label: AppLocalizations.of(context)!.quiet, ), - _LegendChip(color: Colors.orange, label: AppLocalizations.of(context)!.moderate), + _LegendChip( + color: Colors.orange, + label: AppLocalizations.of(context)!.moderate, + ), _LegendChip( color: theme.colorScheme.error, label: AppLocalizations.of(context)!.busy, @@ -147,7 +150,7 @@ class SpectrumScanPanel extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( - color: theme.colorScheme.surface.withValues(alpha: 0.7), + color: theme.colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(12), border: Border.all(color: theme.colorScheme.outlineVariant), ), @@ -303,12 +306,7 @@ class _FrequencyRangePreview extends StatelessWidget { height: 108, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - gradient: LinearGradient( - colors: [ - scheme.surface, - scheme.surfaceContainerHighest.withValues(alpha: 0.9), - ], - ), + color: scheme.surfaceContainerLow, border: Border.all(color: scheme.outlineVariant), ), child: Stack( @@ -322,13 +320,7 @@ class _FrequencyRangePreview extends StatelessWidget { child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - gradient: LinearGradient( - colors: [ - scheme.primary.withValues(alpha: 0.12), - scheme.tertiary.withValues(alpha: 0.08), - scheme.primary.withValues(alpha: 0.12), - ], - ), + color: scheme.surfaceContainerHigh, ), ), ), @@ -341,7 +333,7 @@ class _FrequencyRangePreview extends StatelessWidget { child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: scheme.primary.withValues(alpha: 0.20), + color: scheme.primaryContainer, border: Border.all(color: scheme.primary), ), ), @@ -355,7 +347,7 @@ class _FrequencyRangePreview extends StatelessWidget { child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(999), - color: scheme.tertiary.withValues(alpha: 0.32), + color: scheme.tertiaryContainer, border: Border.all(color: scheme.tertiary), ), ), diff --git a/lib/widgets/messages/recipient_selector_sheet.dart b/lib/widgets/messages/recipient_selector_sheet.dart index aa4c593..15999d4 100644 --- a/lib/widgets/messages/recipient_selector_sheet.dart +++ b/lib/widgets/messages/recipient_selector_sheet.dart @@ -4,7 +4,7 @@ import '../../l10n/app_localizations.dart'; import '../../models/contact.dart'; import '../common/contact_avatar.dart'; -enum _RecipientSortMode { activity, alphabetical } +enum _RecipientSortMode { activity, favorites, alphabetical } /// Bottom sheet for selecting message recipient (channel, contact, or room) class RecipientSelectorSheet extends StatefulWidget { @@ -74,11 +74,27 @@ class _RecipientSelectorSheetState extends State { return a.isPublicChannel ? -1 : 1; } - if (_sortMode == _RecipientSortMode.activity) { + if (_sortMode == _RecipientSortMode.favorites) { + if (a.isFavourite != b.isFavourite) { + return a.isFavourite ? -1 : 1; + } final unreadCompare = _unreadFor(b).compareTo(_unreadFor(a)); if (unreadCompare != 0) { return unreadCompare; } + final lastSeenCompare = b.lastSeenTime.compareTo(a.lastSeenTime); + if (lastSeenCompare != 0) { + return lastSeenCompare; + } + } else if (_sortMode == _RecipientSortMode.activity) { + final unreadCompare = _unreadFor(b).compareTo(_unreadFor(a)); + if (unreadCompare != 0) { + return unreadCompare; + } + final lastSeenCompare = b.lastSeenTime.compareTo(a.lastSeenTime); + if (lastSeenCompare != 0) { + return lastSeenCompare; + } } return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()); @@ -165,7 +181,7 @@ class _RecipientSelectorSheetState extends State { mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 10), child: Column( children: [ Container( @@ -176,64 +192,16 @@ class _RecipientSelectorSheetState extends State { borderRadius: BorderRadius.circular(999), ), ), - const SizedBox(height: 16), + const SizedBox(height: 12), Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.selectRecipient, - style: Theme.of(context).textTheme.titleLarge - ?.copyWith( - fontWeight: FontWeight.w900, - letterSpacing: -0.5, - ), - ), - const SizedBox(height: 10), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - if (widget.showAllOption) - _SummaryChip( - icon: Icons.all_inbox_rounded, - label: l10n.showAll, - count: widget.unreadCount, - accentColor: colorScheme.primary, - ), - if (hasChannels) - _SummaryChip( - icon: Icons.broadcast_on_personal_rounded, - label: l10n.channels, - count: filteredChannels.length, - accentColor: _sectionColor( - context, - 'channel', - ), - ), - if (hasContacts) - _SummaryChip( - icon: Icons.person_rounded, - label: l10n.contacts, - count: filteredContacts.length, - accentColor: _sectionColor( - context, - 'contact', - ), - ), - if (hasRooms) - _SummaryChip( - icon: Icons.meeting_room_outlined, - label: l10n.rooms, - count: filteredRooms.length, - accentColor: _sectionColor(context, 'room'), - ), - ], - ), - ], + child: Text( + l10n.selectRecipient, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + letterSpacing: -0.5, + ), ), ), const SizedBox(width: 12), @@ -244,69 +212,47 @@ class _RecipientSelectorSheetState extends State { ), ], ), - const SizedBox(height: 16), - TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: l10n.searchRecipients, - prefixIcon: const Icon(Icons.search_rounded), - suffixIcon: _searchQuery.isNotEmpty - ? IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - _searchController.clear(); - setState(() { - _searchQuery = ''; - }); - }, - ) - : null, - filled: true, - fillColor: colorScheme.surfaceContainerHigh, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 18, - vertical: 16, - ), - ), - onChanged: (value) { - setState(() { - _searchQuery = value; - }); - }, - ), const SizedBox(height: 12), - Align( - alignment: Alignment.centerLeft, - child: Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _SortChip( - icon: Icons.flash_on_rounded, - label: l10n.active, - selected: _sortMode == _RecipientSortMode.activity, - onTap: () { + Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: l10n.searchRecipients, + prefixIcon: const Icon(Icons.search_rounded), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () { + _searchController.clear(); + setState(() { + _searchQuery = ''; + }); + }, + ) + : null, + filled: true, + fillColor: colorScheme.surfaceContainerHigh, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + onChanged: (value) { setState(() { - _sortMode = _RecipientSortMode.activity; + _searchQuery = value; }); }, ), - _SortChip( - icon: Icons.sort_by_alpha_rounded, - label: 'A-Z', - selected: _sortMode == _RecipientSortMode.alphabetical, - onTap: () { - setState(() { - _sortMode = _RecipientSortMode.alphabetical; - }); - }, - ), - ], - ), + ), + const SizedBox(width: 10), + _buildSortMenuButton(context), + ], ), ], ), @@ -422,6 +368,82 @@ class _RecipientSelectorSheetState extends State { ); } + Widget _buildSortMenuButton(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final colorScheme = Theme.of(context).colorScheme; + + return PopupMenuButton<_RecipientSortMode>( + tooltip: 'Sort', + initialValue: _sortMode, + onSelected: (sortMode) { + setState(() { + _sortMode = sortMode; + }); + }, + itemBuilder: (context) => [ + PopupMenuItem<_RecipientSortMode>( + value: _RecipientSortMode.activity, + child: Row( + children: [ + Icon( + Icons.flash_on_rounded, + size: 18, + color: _sortMode == _RecipientSortMode.activity + ? colorScheme.primary + : null, + ), + const SizedBox(width: 8), + Text(l10n.active), + ], + ), + ), + PopupMenuItem<_RecipientSortMode>( + value: _RecipientSortMode.favorites, + child: Row( + children: [ + Icon( + Icons.star_rounded, + size: 18, + color: _sortMode == _RecipientSortMode.favorites + ? colorScheme.primary + : null, + ), + const SizedBox(width: 8), + Text(l10n.favourites), + ], + ), + ), + const PopupMenuItem<_RecipientSortMode>( + value: _RecipientSortMode.alphabetical, + child: Row( + children: [ + Icon(Icons.sort_by_alpha_rounded, size: 18), + SizedBox(width: 8), + Text('A-Z'), + ], + ), + ), + ], + child: Container( + width: 46, + height: 46, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + ), + alignment: Alignment.center, + child: Icon( + _sortMode == _RecipientSortMode.activity + ? Icons.flash_on_rounded + : _sortMode == _RecipientSortMode.favorites + ? Icons.star_rounded + : Icons.sort_by_alpha_rounded, + color: colorScheme.primary, + ), + ), + ); + } + Widget _buildSectionCard( BuildContext context, { required String type, @@ -438,15 +460,7 @@ class _RecipientSelectorSheetState extends State { margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(14), decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.surface, - accentColor.withValues(alpha: 0.05), - colorScheme.surfaceContainerLow, - ], - ), + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(24), border: Border.all(color: accentColor.withValues(alpha: 0.14)), ), @@ -530,14 +544,9 @@ class _RecipientSelectorSheetState extends State { return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.primaryContainer.withValues(alpha: 0.4), - colorScheme.surfaceContainerHigh, - ], - ), + color: isSelected + ? colorScheme.primaryContainer.withValues(alpha: 0.42) + : colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(24), border: Border.all( color: isSelected @@ -750,104 +759,3 @@ class _RecipientSelectorSheetState extends State { ); } } - -class _SummaryChip extends StatelessWidget { - final IconData icon; - final String label; - final int count; - final Color accentColor; - - const _SummaryChip({ - required this.icon, - required this.label, - required this.count, - required this.accentColor, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(999), - border: Border.all(color: accentColor.withValues(alpha: 0.14)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 14, color: accentColor), - const SizedBox(width: 8), - Text( - '$count', - style: Theme.of( - context, - ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800), - ), - const SizedBox(width: 6), - Text( - label, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ); - } -} - -class _SortChip extends StatelessWidget { - final IconData icon; - final String label; - final bool selected; - final VoidCallback onTap; - - const _SortChip({ - required this.icon, - required this.label, - required this.selected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return FilterChip( - selected: selected, - onSelected: (_) => onTap(), - avatar: Icon( - icon, - size: 16, - color: selected - ? colorScheme.onSecondaryContainer - : colorScheme.primary, - ), - label: Text( - label, - style: Theme.of( - context, - ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700), - ), - side: BorderSide( - color: selected - ? colorScheme.secondaryContainer - : colorScheme.outlineVariant.withValues(alpha: 0.35), - ), - selectedColor: colorScheme.secondaryContainer, - checkmarkColor: colorScheme.onSecondaryContainer, - backgroundColor: colorScheme.surfaceContainerLow, - labelStyle: TextStyle( - color: selected - ? colorScheme.onSecondaryContainer - : colorScheme.onSurface, - ), - showCheckmark: false, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)), - ); - } -} diff --git a/test/screens/contacts_tab_test.dart b/test/screens/contacts_tab_test.dart index 2115a7a..7d38e26 100644 --- a/test/screens/contacts_tab_test.dart +++ b/test/screens/contacts_tab_test.dart @@ -7,6 +7,7 @@ import 'package:meshcore_sar_app/l10n/app_localizations.dart'; import 'package:meshcore_sar_app/models/channel.dart'; import 'package:meshcore_sar_app/models/contact.dart'; import 'package:meshcore_sar_app/models/contact_group.dart'; +import 'package:meshcore_sar_app/models/device_info.dart'; import 'package:meshcore_sar_app/providers/connection_provider.dart'; import 'package:meshcore_sar_app/providers/contacts_provider.dart'; import 'package:meshcore_sar_app/providers/map_provider.dart'; @@ -16,6 +17,20 @@ import 'package:meshcore_sar_app/utils/contact_grouping.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +class _FakeConnectionProvider extends ConnectionProvider { + _FakeConnectionProvider({required bool isConnected}) + : _isConnected = isConnected; + + final bool _isConnected; + + @override + DeviceInfo get deviceInfo => DeviceInfo( + connectionState: _isConnected + ? ConnectionState.connected + : ConnectionState.disconnected, + ); +} + void main() { String? clipboardText; @@ -102,8 +117,11 @@ void main() { WidgetTester tester, { List contacts = const [], List savedGroups = const [], + ConnectionProvider? connectionProvider, }) async { final contactsProvider = ContactsProvider(); + final resolvedConnectionProvider = + connectionProvider ?? ConnectionProvider(); for (final contact in contacts) { contactsProvider.addOrUpdateContact(contact); } @@ -118,7 +136,9 @@ void main() { MultiProvider( providers: [ ChangeNotifierProvider.value(value: contactsProvider), - ChangeNotifierProvider(create: (_) => ConnectionProvider()), + ChangeNotifierProvider.value( + value: resolvedConnectionProvider, + ), ChangeNotifierProvider(create: (_) => MessagesProvider()), ChangeNotifierProvider(create: (_) => MapProvider()), ], @@ -264,4 +284,22 @@ void main() { expect(find.text('Sensors'), findsWidgets); expect(find.text('WX Station'), findsOneWidget); }); + + testWidgets( + 'contacts sections do not show sensor or repeater discovery actions', + (tester) async { + await pumpContactsTab( + tester, + contacts: [ + buildRepeater(seed: 70, name: 'Relay 1'), + buildSensor(seed: 71, name: 'WX Station'), + ], + connectionProvider: _FakeConnectionProvider(isConnected: true), + ); + + expect(find.byTooltip('Discover repeaters'), findsNothing); + expect(find.byTooltip('Discover sensors'), findsNothing); + expect(find.byIcon(Icons.radar), findsNothing); + }, + ); }