fix: Simplify device settings styling #123

This commit is contained in:
Janez T
2026-03-19 14:18:29 +01:00
parent 8da69705ee
commit d8cb35c595
6 changed files with 272 additions and 452 deletions

View File

@@ -934,39 +934,6 @@ class _ContactsTabState extends State<ContactsTab> {
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<ConnectionProvider>()
.discoverNodeType(advertType: 2);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
l10n.repeaterDiscoverySent,
),
),
);
},
),
],
),
),
_buildSectionFilterField(
context,
@@ -1029,37 +996,6 @@ class _ContactsTabState extends State<ContactsTab> {
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<ConnectionProvider>()
.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),
),

View File

@@ -729,9 +729,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
} 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<DeviceConfigScreen> {
.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<DeviceConfigScreen> {
.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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
: _saveAutoDiscoverySettings,
isSaving: _isSavingAutoDiscoverySettings,
isSaved: _autoDiscoverySettingsSaved,
label: AppLocalizations.of(context)!.saveDiscoverySettings,
label: AppLocalizations.of(
context,
)!.saveDiscoverySettings,
),
),
],
@@ -1338,7 +1339,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
'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<DeviceConfigScreen> {
),
)
: 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<DeviceConfigScreen> {
),
)
: 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<DeviceConfigScreen> {
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),

View File

@@ -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),
),

View File

@@ -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),
),
),

View File

@@ -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<RecipientSelectorSheet> {
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<RecipientSelectorSheet> {
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,65 +192,17 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
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(
child: Text(
l10n.selectRecipient,
style: Theme.of(context).textTheme.titleLarge
?.copyWith(
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'),
),
],
),
],
),
),
const SizedBox(width: 12),
IconButton.filledTonal(
@@ -244,8 +212,11 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
),
],
),
const SizedBox(height: 16),
TextField(
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: l10n.searchRecipients,
@@ -264,12 +235,12 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
filled: true,
fillColor: colorScheme.surfaceContainerHigh,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 16,
horizontal: 16,
vertical: 14,
),
),
onChanged: (value) {
@@ -278,36 +249,11 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
});
},
),
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: () {
setState(() {
_sortMode = _RecipientSortMode.activity;
});
},
),
_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<RecipientSelectorSheet> {
);
}
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<RecipientSelectorSheet> {
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<RecipientSelectorSheet> {
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<RecipientSelectorSheet> {
);
}
}
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)),
);
}
}

View File

@@ -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<Contact> contacts = const [],
List<SavedContactGroup> 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<ConnectionProvider>.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);
},
);
}