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, context,
ContactSection.repeaters, 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( _buildSectionFilterField(
context, context,
@@ -1029,37 +996,6 @@ class _ContactsTabState extends State<ContactsTab> {
context, context,
ContactSection.sensors, 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( _buildSectionFilterField(
context, context,
@@ -1718,15 +1654,7 @@ class _SectionCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( color: colorScheme.surfaceContainerLow,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.surface,
accentColor.withValues(alpha: 0.04),
colorScheme.surfaceContainerLow,
],
),
borderRadius: BorderRadius.circular(26), borderRadius: BorderRadius.circular(26),
border: Border.all(color: accentColor.withValues(alpha: 0.14)), border: Border.all(color: accentColor.withValues(alpha: 0.14)),
boxShadow: [ boxShadow: [
@@ -1903,14 +1831,7 @@ class _ChannelActivityCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
gradient: LinearGradient( color: colorScheme.surfaceContainerLow,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.surfaceContainerLow,
colorScheme.surfaceContainerHighest.withValues(alpha: 0.9),
],
),
border: Border.all( border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.35), color: colorScheme.outlineVariant.withValues(alpha: 0.35),
), ),

View File

@@ -729,9 +729,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
setState(() => _gpsLoading = false); setState(() => _gpsLoading = false);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text('Failed to set GPS mode: $e')), context,
); ).showSnackBar(SnackBar(content: Text('Failed to set GPS mode: $e')));
} }
} }
@@ -893,7 +893,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
.toList(); .toList();
if (contacts.isEmpty) { if (contacts.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context)!.noDeviceContactsToClear)), SnackBar(
content: Text(AppLocalizations.of(context)!.noDeviceContactsToClear),
),
); );
return; return;
} }
@@ -988,7 +990,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
.toList(); .toList();
if (channels.isEmpty) { if (channels.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context)!.noCustomChannelsToClear)), SnackBar(
content: Text(AppLocalizations.of(context)!.noCustomChannelsToClear),
),
); );
return; return;
} }
@@ -1095,19 +1099,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)), appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)),
body: Container( body: ColoredBox(
decoration: BoxDecoration( color: colorScheme.surface,
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],
),
),
child: SafeArea( child: SafeArea(
child: ListView( child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
@@ -1146,7 +1139,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
SizedBox(height: 20), SizedBox(height: 20),
_ConfigSectionCard( _ConfigSectionCard(
title: AppLocalizations.of(context)!.storage, title: AppLocalizations.of(context)!.storage,
subtitle: AppLocalizations.of(context)!.availableSpaceOnThisDevice, subtitle: AppLocalizations.of(
context,
)!.availableSpaceOnThisDevice,
icon: Icons.storage_rounded, icon: Icons.storage_rounded,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1190,7 +1185,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
icon: _autoAddDiscoveredContactsEnabled icon: _autoAddDiscoveredContactsEnabled
? Icons.person_add_alt_1 ? Icons.person_add_alt_1
: Icons.person_add_disabled, : Icons.person_add_disabled,
title: AppLocalizations.of(context)!.enableAutomaticAdding, title: AppLocalizations.of(
context,
)!.enableAutomaticAdding,
description: description:
'Turn this off to keep discoveries manual-only on the radio.', 'Turn this off to keep discoveries manual-only on the radio.',
accentColor: _autoAddDiscoveredContactsEnabled accentColor: _autoAddDiscoveredContactsEnabled
@@ -1293,7 +1290,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
SizedBox(height: 14), SizedBox(height: 14),
_SettingHighlightCard( _SettingHighlightCard(
icon: Icons.history_toggle_off_rounded, icon: Icons.history_toggle_off_rounded,
title: AppLocalizations.of(context)!.overwriteOldestWhenFull, title: AppLocalizations.of(
context,
)!.overwriteOldestWhenFull,
description: description:
'Allow the radio to replace the oldest contact when storage is full.', 'Allow the radio to replace the oldest contact when storage is full.',
accentColor: _overwriteOldestAutoAddEnabled accentColor: _overwriteOldestAutoAddEnabled
@@ -1329,7 +1328,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
: _saveAutoDiscoverySettings, : _saveAutoDiscoverySettings,
isSaving: _isSavingAutoDiscoverySettings, isSaving: _isSavingAutoDiscoverySettings,
isSaved: _autoDiscoverySettingsSaved, isSaved: _autoDiscoverySettingsSaved,
label: AppLocalizations.of(context)!.saveDiscoverySettings, label: AppLocalizations.of(
context,
)!.saveDiscoverySettings,
), ),
), ),
], ],
@@ -1338,7 +1339,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
SizedBox(height: 20), SizedBox(height: 20),
_ConfigSectionCard( _ConfigSectionCard(
title: AppLocalizations.of(context)!.publicInfo, title: AppLocalizations.of(context)!.publicInfo,
subtitle: AppLocalizations.of(context)!.chooseTheNameAndLocationThisDeviceShares, subtitle: AppLocalizations.of(
context,
)!.chooseTheNameAndLocationThisDeviceShares,
icon: Icons.public_rounded, icon: Icons.public_rounded,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1368,9 +1371,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
if (_gpsEnabled != null) ...[ if (_gpsEnabled != null) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
_SettingHighlightCard( _SettingHighlightCard(
icon: _gpsEnabled! icon: _gpsEnabled! ? Icons.gps_fixed : Icons.gps_off,
? Icons.gps_fixed
: Icons.gps_off,
title: AppLocalizations.of(context)!.gpsModule, title: AppLocalizations.of(context)!.gpsModule,
description: description:
'Enable or disable the onboard GPS hardware.', 'Enable or disable the onboard GPS hardware.',
@@ -1411,13 +1412,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
Container( Container(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.surface.withValues(alpha: 0.65), color: colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
border: Border.all( border: Border.all(color: colorScheme.outlineVariant),
color: colorScheme.outlineVariant.withValues(
alpha: 0.7,
),
),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1492,7 +1489,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
SizedBox(height: 20), SizedBox(height: 20),
_ConfigSectionCard( _ConfigSectionCard(
title: AppLocalizations.of(context)!.radioSettings, title: AppLocalizations.of(context)!.radioSettings,
subtitle: AppLocalizations.of(context)!.chooseAPresetOrFinetuneCustomRadioSettings, subtitle: AppLocalizations.of(
context,
)!.chooseAPresetOrFinetuneCustomRadioSettings,
icon: Icons.settings_input_antenna_rounded, icon: Icons.settings_input_antenna_rounded,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1716,7 +1715,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
SizedBox(height: 16), SizedBox(height: 16),
_SettingHighlightCard( _SettingHighlightCard(
icon: Icons.repeat_rounded, icon: Icons.repeat_rounded,
title: AppLocalizations.of(context)!.repeatNearbyTraffic, title: AppLocalizations.of(
context,
)!.repeatNearbyTraffic,
description: description:
deviceInfo.allowedRepeatFreqRanges != null && deviceInfo.allowedRepeatFreqRanges != null &&
deviceInfo.allowedRepeatFreqRanges!.isNotEmpty deviceInfo.allowedRepeatFreqRanges!.isNotEmpty
@@ -1762,7 +1763,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
SizedBox(height: 20), SizedBox(height: 20),
_ConfigSectionCard( _ConfigSectionCard(
title: AppLocalizations.of(context)!.dangerZone, title: AppLocalizations.of(context)!.dangerZone,
subtitle: AppLocalizations.of(context)!.destructiveDeviceActions, subtitle: AppLocalizations.of(
context,
)!.destructiveDeviceActions,
icon: Icons.warning_amber_rounded, icon: Icons.warning_amber_rounded,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1770,13 +1773,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
Container( Container(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.errorContainer.withValues( color: colorScheme.surfaceContainerLow,
alpha: 0.55,
),
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
border: Border.all( border: Border.all(color: colorScheme.error),
color: colorScheme.error.withValues(alpha: 0.28),
),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1794,14 +1793,14 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
'Wipe data on device', 'Wipe data on device',
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
color: colorScheme.onErrorContainer, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Erase contacts, keys, and radio settings from the connected MeshCore device and return it to factory defaults.', 'Erase contacts, keys, and radio settings from the connected MeshCore device and return it to factory defaults.',
style: theme.textTheme.bodySmall?.copyWith( 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), : Icon(Icons.people_alt_outlined),
label: Text(AppLocalizations.of(context)!.clearAllContacts), label: Text(
AppLocalizations.of(context)!.clearAllContacts,
),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -1855,7 +1856,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
), ),
) )
: Icon(Icons.forum_outlined), : Icon(Icons.forum_outlined),
label: Text(AppLocalizations.of(context)!.clearAllChannels), label: Text(
AppLocalizations.of(context)!.clearAllChannels,
),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -1871,7 +1874,9 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
minimumSize: const Size.fromHeight(52), minimumSize: const Size.fromHeight(52),
), ),
icon: Icon(Icons.delete_forever_rounded), 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( return Container(
padding: const EdgeInsets.all(22), padding: const EdgeInsets.all(22),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( color: colorScheme.surfaceContainerLow,
colors: [
colorScheme.primaryContainer,
colorScheme.primary.withValues(alpha: 0.14),
colorScheme.surfaceContainerHighest,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
border: Border.all( border: Border.all(color: colorScheme.outlineVariant),
color: colorScheme.onPrimaryContainer.withValues(alpha: 0.08),
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withValues(alpha: 0.10),
blurRadius: 26,
offset: const Offset(0, 12),
),
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1960,13 +1948,10 @@ class _ConfigHeroCard extends StatelessWidget {
width: 56, width: 56,
height: 56, height: 56,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.onPrimaryContainer.withValues(alpha: 0.12), color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
), ),
child: Icon( child: Icon(Icons.tune_rounded, color: colorScheme.primary),
Icons.tune_rounded,
color: colorScheme.onPrimaryContainer,
),
), ),
const SizedBox(width: 14), const SizedBox(width: 14),
Expanded( Expanded(
@@ -1979,15 +1964,13 @@ class _ConfigHeroCard extends StatelessWidget {
vertical: 6, vertical: 6,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.onPrimaryContainer.withValues( color: colorScheme.surfaceContainerHighest,
alpha: 0.10,
),
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
child: Text( child: Text(
'Device settings', 'Device settings',
style: theme.textTheme.labelMedium?.copyWith( style: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onPrimaryContainer, color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
@@ -1997,16 +1980,14 @@ class _ConfigHeroCard extends StatelessWidget {
title, title,
style: theme.textTheme.headlineSmall?.copyWith( style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
color: colorScheme.onPrimaryContainer, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
subtitle, subtitle,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onPrimaryContainer.withValues( color: colorScheme.onSurfaceVariant,
alpha: 0.82,
),
height: 1.35, height: 1.35,
), ),
), ),
@@ -2050,23 +2031,24 @@ class _HeroStat extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
final background = data.emphasized final iconColor = data.emphasized
? colorScheme.primary.withValues(alpha: 0.18) ? colorScheme.primary
: colorScheme.onPrimaryContainer.withValues(alpha: 0.10); : colorScheme.onSurfaceVariant;
final borderColor = data.emphasized
? colorScheme.primary
: colorScheme.outlineVariant;
return Container( return Container(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: background, color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(color: borderColor),
color: colorScheme.onPrimaryContainer.withValues(alpha: 0.08),
),
), ),
child: Row( child: Row(
children: [ children: [
Icon(data.icon, size: 18, color: colorScheme.onPrimaryContainer), Icon(data.icon, size: 18, color: iconColor),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Column( child: Column(
@@ -2075,9 +2057,7 @@ class _HeroStat extends StatelessWidget {
Text( Text(
data.label, data.label,
style: theme.textTheme.labelMedium?.copyWith( style: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onPrimaryContainer.withValues( color: colorScheme.onSurfaceVariant,
alpha: 0.76,
),
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -2086,7 +2066,7 @@ class _HeroStat extends StatelessWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
color: colorScheme.onPrimaryContainer, color: colorScheme.onSurface,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
), ),
@@ -2118,7 +2098,9 @@ class _ConfigSectionCard extends StatelessWidget {
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
return Card( return Card(
color: colorScheme.surfaceContainerLow,
elevation: 0, elevation: 0,
surfaceTintColor: Colors.transparent,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
child: Padding( child: Padding(
@@ -2133,7 +2115,7 @@ class _ConfigSectionCard extends StatelessWidget {
width: 42, width: 42,
height: 42, height: 42,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.10), color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
), ),
child: Icon(icon, color: colorScheme.primary), child: Icon(icon, color: colorScheme.primary),
@@ -2259,14 +2241,7 @@ class _StorageUsageMeter extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( color: colorScheme.surfaceContainerLow,
colors: [
colorScheme.surfaceContainerHighest.withValues(alpha: 0.65),
colorScheme.surfaceContainer,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
border: Border.all(color: colorScheme.outlineVariant), border: Border.all(color: colorScheme.outlineVariant),
), ),
@@ -2321,9 +2296,9 @@ class _SelectedPresetCard extends StatelessWidget {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.55), color: colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: colorScheme.primary.withValues(alpha: 0.18)), border: Border.all(color: colorScheme.outlineVariant),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -2418,16 +2393,9 @@ class _SettingHighlightCard extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( color: colorScheme.surfaceContainerLow,
colors: [
accentColor.withValues(alpha: 0.14),
colorScheme.surfaceContainerHigh.withValues(alpha: 0.85),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
border: Border.all(color: accentColor.withValues(alpha: 0.18)), border: Border.all(color: colorScheme.outlineVariant),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -2436,7 +2404,7 @@ class _SettingHighlightCard extends StatelessWidget {
width: 42, width: 42,
height: 42, height: 42,
decoration: BoxDecoration( decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.16), color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
), ),
child: Icon(icon, color: accentColor), child: Icon(icon, color: accentColor),

View File

@@ -185,14 +185,7 @@ class ContactTile extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
gradient: LinearGradient( color: colorScheme.surfaceContainerLow,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.surfaceContainerLow,
colorScheme.surfaceContainerHighest.withValues(alpha: 0.9),
],
),
border: Border.all( border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.35), color: colorScheme.outlineVariant.withValues(alpha: 0.35),
), ),

View File

@@ -106,7 +106,10 @@ class SpectrumScanPanel extends StatelessWidget {
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
label: AppLocalizations.of(context)!.quiet, label: AppLocalizations.of(context)!.quiet,
), ),
_LegendChip(color: Colors.orange, label: AppLocalizations.of(context)!.moderate), _LegendChip(
color: Colors.orange,
label: AppLocalizations.of(context)!.moderate,
),
_LegendChip( _LegendChip(
color: theme.colorScheme.error, color: theme.colorScheme.error,
label: AppLocalizations.of(context)!.busy, label: AppLocalizations.of(context)!.busy,
@@ -147,7 +150,7 @@ class SpectrumScanPanel extends StatelessWidget {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surface.withValues(alpha: 0.7), color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: theme.colorScheme.outlineVariant), border: Border.all(color: theme.colorScheme.outlineVariant),
), ),
@@ -303,12 +306,7 @@ class _FrequencyRangePreview extends StatelessWidget {
height: 108, height: 108,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
gradient: LinearGradient( color: scheme.surfaceContainerLow,
colors: [
scheme.surface,
scheme.surfaceContainerHighest.withValues(alpha: 0.9),
],
),
border: Border.all(color: scheme.outlineVariant), border: Border.all(color: scheme.outlineVariant),
), ),
child: Stack( child: Stack(
@@ -322,13 +320,7 @@ class _FrequencyRangePreview extends StatelessWidget {
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
gradient: LinearGradient( color: scheme.surfaceContainerHigh,
colors: [
scheme.primary.withValues(alpha: 0.12),
scheme.tertiary.withValues(alpha: 0.08),
scheme.primary.withValues(alpha: 0.12),
],
),
), ),
), ),
), ),
@@ -341,7 +333,7 @@ class _FrequencyRangePreview extends StatelessWidget {
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
color: scheme.primary.withValues(alpha: 0.20), color: scheme.primaryContainer,
border: Border.all(color: scheme.primary), border: Border.all(color: scheme.primary),
), ),
), ),
@@ -355,7 +347,7 @@ class _FrequencyRangePreview extends StatelessWidget {
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
color: scheme.tertiary.withValues(alpha: 0.32), color: scheme.tertiaryContainer,
border: Border.all(color: scheme.tertiary), border: Border.all(color: scheme.tertiary),
), ),
), ),

View File

@@ -4,7 +4,7 @@ import '../../l10n/app_localizations.dart';
import '../../models/contact.dart'; import '../../models/contact.dart';
import '../common/contact_avatar.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) /// Bottom sheet for selecting message recipient (channel, contact, or room)
class RecipientSelectorSheet extends StatefulWidget { class RecipientSelectorSheet extends StatefulWidget {
@@ -74,11 +74,27 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
return a.isPublicChannel ? -1 : 1; 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)); final unreadCompare = _unreadFor(b).compareTo(_unreadFor(a));
if (unreadCompare != 0) { if (unreadCompare != 0) {
return unreadCompare; 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()); return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase());
@@ -165,7 +181,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), padding: const EdgeInsets.fromLTRB(16, 12, 16, 10),
child: Column( child: Column(
children: [ children: [
Container( Container(
@@ -176,65 +192,17 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: Column( child: Text(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.selectRecipient, l10n.selectRecipient,
style: Theme.of(context).textTheme.titleLarge style: Theme.of(context).textTheme.titleLarge?.copyWith(
?.copyWith(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
letterSpacing: -0.5, 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), const SizedBox(width: 12),
IconButton.filledTonal( IconButton.filledTonal(
@@ -244,8 +212,11 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
TextField( Row(
children: [
Expanded(
child: TextField(
controller: _searchController, controller: _searchController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: l10n.searchRecipients, hintText: l10n.searchRecipients,
@@ -264,12 +235,12 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
filled: true, filled: true,
fillColor: colorScheme.surfaceContainerHigh, fillColor: colorScheme.surfaceContainerHigh,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 18, horizontal: 16,
vertical: 16, vertical: 14,
), ),
), ),
onChanged: (value) { 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( Widget _buildSectionCard(
BuildContext context, { BuildContext context, {
required String type, required String type,
@@ -438,15 +460,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( color: colorScheme.surfaceContainerLow,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.surface,
accentColor.withValues(alpha: 0.05),
colorScheme.surfaceContainerLow,
],
),
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
border: Border.all(color: accentColor.withValues(alpha: 0.14)), border: Border.all(color: accentColor.withValues(alpha: 0.14)),
), ),
@@ -530,14 +544,9 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
return Container( return Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( color: isSelected
begin: Alignment.topLeft, ? colorScheme.primaryContainer.withValues(alpha: 0.42)
end: Alignment.bottomRight, : colorScheme.surfaceContainerHigh,
colors: [
colorScheme.primaryContainer.withValues(alpha: 0.4),
colorScheme.surfaceContainerHigh,
],
),
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
border: Border.all( border: Border.all(
color: isSelected 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/channel.dart';
import 'package:meshcore_sar_app/models/contact.dart'; import 'package:meshcore_sar_app/models/contact.dart';
import 'package:meshcore_sar_app/models/contact_group.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/connection_provider.dart';
import 'package:meshcore_sar_app/providers/contacts_provider.dart'; import 'package:meshcore_sar_app/providers/contacts_provider.dart';
import 'package:meshcore_sar_app/providers/map_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:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.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() { void main() {
String? clipboardText; String? clipboardText;
@@ -102,8 +117,11 @@ void main() {
WidgetTester tester, { WidgetTester tester, {
List<Contact> contacts = const [], List<Contact> contacts = const [],
List<SavedContactGroup> savedGroups = const [], List<SavedContactGroup> savedGroups = const [],
ConnectionProvider? connectionProvider,
}) async { }) async {
final contactsProvider = ContactsProvider(); final contactsProvider = ContactsProvider();
final resolvedConnectionProvider =
connectionProvider ?? ConnectionProvider();
for (final contact in contacts) { for (final contact in contacts) {
contactsProvider.addOrUpdateContact(contact); contactsProvider.addOrUpdateContact(contact);
} }
@@ -118,7 +136,9 @@ void main() {
MultiProvider( MultiProvider(
providers: [ providers: [
ChangeNotifierProvider.value(value: contactsProvider), ChangeNotifierProvider.value(value: contactsProvider),
ChangeNotifierProvider(create: (_) => ConnectionProvider()), ChangeNotifierProvider<ConnectionProvider>.value(
value: resolvedConnectionProvider,
),
ChangeNotifierProvider(create: (_) => MessagesProvider()), ChangeNotifierProvider(create: (_) => MessagesProvider()),
ChangeNotifierProvider(create: (_) => MapProvider()), ChangeNotifierProvider(create: (_) => MapProvider()),
], ],
@@ -264,4 +284,22 @@ void main() {
expect(find.text('Sensors'), findsWidgets); expect(find.text('Sensors'), findsWidgets);
expect(find.text('WX Station'), findsOneWidget); 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);
},
);
} }