Clarify path_provider override

This commit is contained in:
Janez T
2026-03-08 09:30:30 +01:00
parent cd508d5f74
commit 05153c9ceb
8 changed files with 564 additions and 215 deletions

View File

@@ -489,7 +489,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 104; CURRENT_PROJECT_VERSION = 105;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -511,7 +511,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 104; CURRENT_PROJECT_VERSION = 105;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -530,7 +530,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 104; CURRENT_PROJECT_VERSION = 105;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -547,7 +547,7 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 104; CURRENT_PROJECT_VERSION = 105;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@@ -679,7 +679,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 104; CURRENT_PROJECT_VERSION = 105;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -702,7 +702,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 104; CURRENT_PROJECT_VERSION = 105;
DEVELOPMENT_TEAM = JND55328G8; DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -43,7 +43,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>104</string> <string>105</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -5,22 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000221"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.001342">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.414982"> <testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.40706">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="89.53674"> <testcase classname="fastlane.lanes" name="2: build_app" time="70.83361">
</testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="242.066948">
</testcase> </testcase>

View File

@@ -11,6 +11,7 @@ import 'contacts_provider.dart';
enum SensorRefreshState { idle, refreshing, success, timeout, unavailable } enum SensorRefreshState { idle, refreshing, success, timeout, unavailable }
class SensorsProvider with ChangeNotifier { class SensorsProvider with ChangeNotifier {
static const Duration _successStateRetention = Duration(minutes: 1);
static const String _watchedSensorsKey = 'watched_sensor_keys'; static const String _watchedSensorsKey = 'watched_sensor_keys';
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics'; static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
static const String _fieldSpanKey = 'sensor_field_spans'; static const String _fieldSpanKey = 'sensor_field_spans';
@@ -26,6 +27,7 @@ class SensorsProvider with ChangeNotifier {
final List<String> _watchedSensorKeys = <String>[]; final List<String> _watchedSensorKeys = <String>[];
final Map<String, SensorRefreshState> _refreshStates = final Map<String, SensorRefreshState> _refreshStates =
<String, SensorRefreshState>{}; <String, SensorRefreshState>{};
final Map<String, DateTime> _refreshStateUpdatedAt = <String, DateTime>{};
final Map<String, Set<String>> _visibleFieldsBySensor = final Map<String, Set<String>> _visibleFieldsBySensor =
<String, Set<String>>{}; <String, Set<String>>{};
final Map<String, Map<String, int>> _fieldSpansBySensor = final Map<String, Map<String, int>> _fieldSpansBySensor =
@@ -189,6 +191,7 @@ class SensorsProvider with ChangeNotifier {
Future<void> removeSensor(String publicKeyHex) async { Future<void> removeSensor(String publicKeyHex) async {
_watchedSensorKeys.remove(publicKeyHex); _watchedSensorKeys.remove(publicKeyHex);
_refreshStates.remove(publicKeyHex); _refreshStates.remove(publicKeyHex);
_refreshStateUpdatedAt.remove(publicKeyHex);
_visibleFieldsBySensor.remove(publicKeyHex); _visibleFieldsBySensor.remove(publicKeyHex);
_fieldSpansBySensor.remove(publicKeyHex); _fieldSpansBySensor.remove(publicKeyHex);
await _persistWatchedSensors(); await _persistWatchedSensors();
@@ -246,22 +249,52 @@ class SensorsProvider with ChangeNotifier {
} }
if (contact == null) { if (contact == null) {
_refreshStates[publicKeyHex] = SensorRefreshState.unavailable; _setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
notifyListeners();
return; return;
} }
_refreshStates[publicKeyHex] = SensorRefreshState.refreshing; _setRefreshState(publicKeyHex, SensorRefreshState.refreshing);
notifyListeners();
final result = await connectionProvider.smartPing( final result = await connectionProvider.smartPing(
contactPublicKey: contact.publicKey, contactPublicKey: contact.publicKey,
hasPath: contact.hasPath, hasPath: contact.hasPath,
); );
_refreshStates[publicKeyHex] = result.success _setRefreshState(
? SensorRefreshState.success publicKeyHex,
: SensorRefreshState.timeout; result.success ? SensorRefreshState.success : SensorRefreshState.timeout,
);
}
void clearExpiredRefreshStates({DateTime? now}) {
final cutoff = (now ?? DateTime.now()).subtract(_successStateRetention);
final keysToClear = <String>[];
for (final entry in _refreshStates.entries) {
if (entry.value != SensorRefreshState.success) {
continue;
}
final updatedAt = _refreshStateUpdatedAt[entry.key];
if (updatedAt == null || !updatedAt.isAfter(cutoff)) {
keysToClear.add(entry.key);
}
}
if (keysToClear.isEmpty) {
return;
}
for (final key in keysToClear) {
_refreshStates.remove(key);
_refreshStateUpdatedAt.remove(key);
}
notifyListeners();
}
void _setRefreshState(String publicKeyHex, SensorRefreshState state) {
_refreshStates[publicKeyHex] = state;
_refreshStateUpdatedAt[publicKeyHex] = DateTime.now();
notifyListeners(); notifyListeners();
} }
} }

View File

@@ -398,26 +398,51 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final deviceInfo = context.watch<ConnectionProvider>().deviceInfo; final deviceInfo = context.watch<ConnectionProvider>().deviceInfo;
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final locationSet =
(deviceInfo.advLat != null && deviceInfo.advLat != 0) ||
(deviceInfo.advLon != null && deviceInfo.advLon != 0);
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)), appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)),
body: ListView( body: SafeArea(
padding: const EdgeInsets.all(16), child: ListView(
children: [ padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
// Device Info Card children: [
Card( _ConfigHeroCard(
child: Padding( title: deviceInfo.selfName ?? deviceInfo.deviceName ?? 'MeshCore',
padding: const EdgeInsets.all(16), subtitle:
'${_getDeviceTypeString(context, deviceInfo.deviceType)}${deviceInfo.semanticVersion ?? deviceInfo.manufacturerModel ?? AppLocalizations.of(context)!.unknown}',
chips: [
_StatusChipData(
icon: Icons.bluetooth,
label:
'${AppLocalizations.of(context)!.bleName}: ${deviceInfo.deviceName ?? AppLocalizations.of(context)!.unknown}',
),
_StatusChipData(
icon: Icons.my_location,
label: locationSet ? 'Location ready' : 'Location off',
emphasized: locationSet,
),
_StatusChipData(
icon: Icons.settings_input_antenna,
label: '${_freqController.text} MHz • $_selectedBandwidth',
),
_StatusChipData(
icon: Icons.key,
label: 'FW v${deviceInfo.firmwareVersion?.toString() ?? "?"}',
),
],
),
const SizedBox(height: 20),
_ConfigSectionCard(
title: AppLocalizations.of(context)!.deviceInformation,
subtitle:
'${deviceInfo.manufacturerModel ?? AppLocalizations.of(context)!.unknown}${deviceInfo.firmwareBuildDate ?? AppLocalizations.of(context)!.unknown}',
icon: Icons.memory_rounded,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(
AppLocalizations.of(context)!.deviceInformation,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_InfoRow( _InfoRow(
AppLocalizations.of(context)!.bleName, AppLocalizations.of(context)!.bleName,
deviceInfo.deviceName ?? deviceInfo.deviceName ??
@@ -467,42 +492,75 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
], ],
), ),
), ),
), const SizedBox(height: 20),
_ConfigSectionCard(
const SizedBox(height: 24), title: AppLocalizations.of(context)!.publicInfo,
subtitle: AppLocalizations.of(context)!.nameBroadcastInMesh,
// Public Info Section icon: Icons.public_rounded,
Card( trailing: FilledButton.icon(
child: Padding( onPressed: _savePublicInfo,
padding: const EdgeInsets.all(16), icon: const Icon(Icons.save_outlined),
label: Text(AppLocalizations.of(context)!.save),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Container(
mainAxisAlignment: MainAxisAlignment.spaceBetween, padding: const EdgeInsets.all(14),
children: [ decoration: BoxDecoration(
Text( color: colorScheme.surfaceContainerHighest.withValues(
AppLocalizations.of(context)!.publicInfo, alpha: 0.45,
style: theme.textTheme.titleLarge?.copyWith( ),
fontWeight: FontWeight.bold, borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
Icon(
_telemetryEnabled
? Icons.travel_explore
: Icons.location_disabled,
color: _telemetryEnabled
? colorScheme.primary
: colorScheme.onSurfaceVariant,
), ),
), const SizedBox(width: 12),
Row( Expanded(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(width: 8), children: [
IconButton.filled( Text(
onPressed: _savePublicInfo, AppLocalizations.of(
icon: const Icon(Icons.save), context,
tooltip: AppLocalizations.of(context)!.save, )!.telemetryAndLocationSharing,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
_telemetryEnabled
? 'This device advertises position data to the mesh.'
: 'Position broadcasting is currently disabled.',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
), ),
], ),
), const SizedBox(width: 12),
], Switch(
value: _telemetryEnabled,
onChanged: (value) {
setState(() {
_telemetryEnabled = value;
});
},
),
],
),
), ),
const SizedBox(height: 16), const SizedBox(height: 18),
// Mesh Network Name
TextField( TextField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
@@ -513,62 +571,21 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
)!.nameBroadcastInMesh, )!.nameBroadcastInMesh,
), ),
), ),
const SizedBox(height: 8),
// Telemetry Toggle - Compact version
Row(
children: [
Expanded(
child: Text(
AppLocalizations.of(
context,
)!.telemetryAndLocationSharing,
style: theme.textTheme.bodyMedium,
),
),
Switch(
value: _telemetryEnabled,
onChanged: (value) {
setState(() {
_telemetryEnabled = value;
});
},
),
],
),
// GPS Coordinates (only show if telemetry enabled)
if (_telemetryEnabled) ...[ if (_telemetryEnabled) ...[
const SizedBox(height: 12), const SizedBox(height: 16),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: _CompactCoordinateField(
controller: _latController, controller: _latController,
decoration: InputDecoration( label: AppLocalizations.of(context)!.lat,
labelText: AppLocalizations.of(context)!.lat,
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: TextField( child: _CompactCoordinateField(
controller: _lonController, controller: _lonController,
decoration: InputDecoration( label: AppLocalizations.of(context)!.lon,
labelText: AppLocalizations.of(context)!.lon,
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -585,36 +602,45 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
], ],
), ),
), ),
), const SizedBox(height: 20),
_ConfigSectionCard(
const SizedBox(height: 24), title: AppLocalizations.of(context)!.radioSettings,
subtitle:
// Radio Settings Section '${_freqController.text} MHz • SF$_selectedSpreadingFactor • CR$_selectedCodingRate',
Card( icon: Icons.settings_input_antenna_rounded,
child: Padding( trailing: FilledButton.icon(
padding: const EdgeInsets.all(16), onPressed: _saveRadioSettings,
icon: const Icon(Icons.save_outlined),
label: Text(AppLocalizations.of(context)!.save),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Expanded(
AppLocalizations.of(context)!.radioSettings, child: _RadioMetricTile(
style: theme.textTheme.titleLarge?.copyWith( label: AppLocalizations.of(context)!.bandwidth,
fontWeight: FontWeight.bold, value: _selectedBandwidth,
), ),
), ),
IconButton.filled( const SizedBox(width: 10),
onPressed: _saveRadioSettings, Expanded(
icon: const Icon(Icons.save), child: _RadioMetricTile(
tooltip: AppLocalizations.of(context)!.save, label: AppLocalizations.of(context)!.spreadingFactor,
value: 'SF$_selectedSpreadingFactor',
),
),
const SizedBox(width: 10),
Expanded(
child: _RadioMetricTile(
label: AppLocalizations.of(context)!.codingRate,
value: 'CR$_selectedCodingRate',
),
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// LoRa Frequency
TextField( TextField(
controller: _freqController, controller: _freqController,
decoration: InputDecoration( decoration: InputDecoration(
@@ -629,8 +655,6 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Bandwidth
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
initialValue: _selectedBandwidth, initialValue: _selectedBandwidth,
decoration: InputDecoration( decoration: InputDecoration(
@@ -652,8 +676,6 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Spreading Factor
DropdownButtonFormField<int>( DropdownButtonFormField<int>(
initialValue: _selectedSpreadingFactor, initialValue: _selectedSpreadingFactor,
decoration: InputDecoration( decoration: InputDecoration(
@@ -677,8 +699,6 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Coding Rate
DropdownButtonFormField<int>( DropdownButtonFormField<int>(
initialValue: _selectedCodingRate, initialValue: _selectedCodingRate,
decoration: InputDecoration( decoration: InputDecoration(
@@ -702,8 +722,6 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// TX Power
TextField( TextField(
controller: _txPowerController, controller: _txPowerController,
decoration: InputDecoration( decoration: InputDecoration(
@@ -715,37 +733,42 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
// Repeat Mode (firmware v9+)
if (deviceInfo.clientRepeat != null) ...[ if (deviceInfo.clientRepeat != null) ...[
const SizedBox(height: 8), const SizedBox(height: 16),
SwitchListTile( Container(
contentPadding: EdgeInsets.zero, padding: const EdgeInsets.all(14),
title: const Text('Client Repeat Mode'), decoration: BoxDecoration(
subtitle: color: colorScheme.surfaceContainerHighest.withValues(
deviceInfo.allowedRepeatFreqRanges != null && alpha: 0.45,
deviceInfo.allowedRepeatFreqRanges!.isNotEmpty ),
? Text( borderRadius: BorderRadius.circular(18),
'Allowed: ${deviceInfo.allowedRepeatFreqRanges!.map((r) => r.lower == r.upper ? '${(r.lower / 1000).toStringAsFixed(3)} MHz' : '${(r.lower / 1000).toStringAsFixed(3)}${(r.upper / 1000).toStringAsFixed(3)} MHz').join(', ')}', ),
) child: SwitchListTile(
: const Text( contentPadding: EdgeInsets.zero,
'Repeat packets on behalf of nearby nodes', title: const Text('Client Repeat Mode'),
), subtitle:
value: _repeatEnabled, deviceInfo.allowedRepeatFreqRanges != null &&
onChanged: (value) { deviceInfo.allowedRepeatFreqRanges!.isNotEmpty
setState(() { ? Text(
_repeatEnabled = value; 'Allowed: ${deviceInfo.allowedRepeatFreqRanges!.map((r) => r.lower == r.upper ? '${(r.lower / 1000).toStringAsFixed(3)} MHz' : '${(r.lower / 1000).toStringAsFixed(3)}${(r.upper / 1000).toStringAsFixed(3)} MHz').join(', ')}',
}); )
}, : const Text(
'Repeat packets on behalf of nearby nodes',
),
value: _repeatEnabled,
onChanged: (value) {
setState(() {
_repeatEnabled = value;
});
},
),
), ),
], ],
], ],
), ),
), ),
), ],
),
const SizedBox(height: 24),
],
), ),
); );
} }
@@ -772,6 +795,276 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
} }
} }
class _ConfigHeroCard extends StatelessWidget {
final String title;
final String subtitle;
final List<_StatusChipData> chips;
const _ConfigHeroCard({
required this.title,
required this.subtitle,
required this.chips,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
colorScheme.primaryContainer,
colorScheme.surfaceContainerHighest,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(28),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: colorScheme.onPrimaryContainer.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(18),
),
child: Icon(
Icons.tune_rounded,
color: colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w800,
color: colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onPrimaryContainer.withValues(
alpha: 0.82,
),
),
),
],
),
),
],
),
const SizedBox(height: 18),
Wrap(
spacing: 10,
runSpacing: 10,
children: chips.map(_StatusChip.new).toList(),
),
],
),
);
}
}
class _ConfigSectionCard extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Widget child;
final Widget? trailing;
const _ConfigSectionCard({
required this.title,
required this.subtitle,
required this.icon,
required this.child,
this.trailing,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Card(
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: colorScheme.primary),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 3),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
if (trailing != null) ...[const SizedBox(width: 12), trailing!],
],
),
const SizedBox(height: 18),
child,
],
),
),
);
}
}
class _StatusChipData {
final IconData icon;
final String label;
final bool emphasized;
const _StatusChipData({
required this.icon,
required this.label,
this.emphasized = false,
});
}
class _StatusChip extends StatelessWidget {
final _StatusChipData data;
const _StatusChip(this.data);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final chipColor = data.emphasized
? colorScheme.primary.withValues(alpha: 0.14)
: colorScheme.onPrimaryContainer.withValues(alpha: 0.10);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: chipColor,
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(data.icon, size: 16, color: colorScheme.onPrimaryContainer),
const SizedBox(width: 8),
Text(
data.label,
style: theme.textTheme.labelLarge?.copyWith(
color: colorScheme.onPrimaryContainer,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
class _CompactCoordinateField extends StatelessWidget {
final TextEditingController controller;
final String label;
const _CompactCoordinateField({
required this.controller,
required this.label,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
);
}
}
class _RadioMetricTile extends StatelessWidget {
final String label;
final String value;
const _RadioMetricTile({required this.label, required this.value});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(18),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 6),
Text(
value,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
),
),
],
),
);
}
}
class _InfoRow extends StatelessWidget { class _InfoRow extends StatelessWidget {
final String label; final String label;
final String value; final String value;

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart' as flutter_map; import 'package:flutter_map/flutter_map.dart' as flutter_map;
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@@ -10,9 +12,53 @@ import '../providers/contacts_provider.dart';
import '../providers/sensors_provider.dart'; import '../providers/sensors_provider.dart';
import '../utils/location_formats.dart'; import '../utils/location_formats.dart';
class SensorsTab extends StatelessWidget { class SensorsTab extends StatefulWidget {
const SensorsTab({super.key}); const SensorsTab({super.key});
@override
State<SensorsTab> createState() => _SensorsTabState();
}
class _SensorsTabState extends State<SensorsTab> {
Timer? _minuteTicker;
@override
void initState() {
super.initState();
_scheduleMinuteTicker();
}
@override
void dispose() {
_minuteTicker?.cancel();
super.dispose();
}
void _scheduleMinuteTicker() {
_minuteTicker?.cancel();
final now = DateTime.now();
final nextMinute = DateTime(
now.year,
now.month,
now.day,
now.hour,
now.minute + 1,
);
final delay = nextMinute.difference(now);
_minuteTicker = Timer(delay, () {
if (!mounted) return;
context.read<SensorsProvider>().clearExpiredRefreshStates();
setState(() {});
_minuteTicker = Timer.periodic(const Duration(minutes: 1), (_) {
if (!mounted) return;
context.read<SensorsProvider>().clearExpiredRefreshStates();
setState(() {});
});
});
}
Future<void> _showAddSensorSheet(BuildContext context) async { Future<void> _showAddSensorSheet(BuildContext context) async {
final sensorsProvider = context.read<SensorsProvider>(); final sensorsProvider = context.read<SensorsProvider>();
final contactsProvider = context.read<ContactsProvider>(); final contactsProvider = context.read<ContactsProvider>();
@@ -588,7 +634,7 @@ class _SensorCard extends StatelessWidget {
String _formatTelemetryTime(DateTime timestamp) { String _formatTelemetryTime(DateTime timestamp) {
final diff = DateTime.now().difference(timestamp); final diff = DateTime.now().difference(timestamp);
if (diff.inMinutes < 1) return 'just now'; if (diff.inMinutes < 1) return 'now';
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
if (diff.inHours < 24) return '${diff.inHours}h ago'; if (diff.inHours < 24) return '${diff.inHours}h ago';
return '${diff.inDays}d ago'; return '${diff.inDays}d ago';
@@ -688,49 +734,33 @@ class _MetricTile extends StatelessWidget {
final location = data.mapLocation; final location = data.mapLocation;
if (location == null) return; if (location == null) return;
await showDialog<void>( await Navigator.of(context).push(
context: context, MaterialPageRoute<void>(
builder: (dialogContext) { builder: (pageContext) {
return Dialog( return Scaffold(
insetPadding: const EdgeInsets.all(16), appBar: AppBar(
clipBehavior: Clip.antiAlias, title: Column(
child: SizedBox( crossAxisAlignment: CrossAxisAlignment.start,
height: 420, children: [
child: Column( Text(data.label),
Text(
data.value,
style: Theme.of(pageContext).textTheme.bodySmall,
),
],
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( if (data.secondaryValue != null)
padding: const EdgeInsets.fromLTRB(16, 14, 8, 8), Padding(
child: Row( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
children: [ child: Text(
Expanded( data.secondaryValue!,
child: Column( style: Theme.of(pageContext).textTheme.bodyMedium,
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [
Text(
data.label,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
data.value,
style: Theme.of(context).textTheme.bodyMedium,
),
if (data.secondaryValue != null)
Text(
data.secondaryValue!,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
IconButton(
onPressed: () => Navigator.of(dialogContext).pop(),
icon: const Icon(Icons.close),
),
],
), ),
),
Expanded( Expanded(
child: flutter_map.FlutterMap( child: flutter_map.FlutterMap(
options: flutter_map.MapOptions( options: flutter_map.MapOptions(
@@ -763,9 +793,10 @@ class _MetricTile extends StatelessWidget {
), ),
], ],
), ),
), );
); },
}, fullscreenDialog: true,
),
); );
} }

View File

@@ -969,7 +969,7 @@ packages:
source: hosted source: hosted
version: "2.2.22" version: "2.2.22"
path_provider_foundation: path_provider_foundation:
dependency: "direct overridden" dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
@@ -1582,5 +1582,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.10.0 <4.0.0" dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.1" flutter: ">=3.38.4"

View File

@@ -138,9 +138,6 @@ dev_dependencies:
flutter_launcher_icons: "^0.14.4" flutter_launcher_icons: "^0.14.4"
fake_async: ^1.3.3 fake_async: ^1.3.3
dependency_overrides:
path_provider_foundation: 2.6.0
flutter_launcher_icons: flutter_launcher_icons:
android: "launcher_icon" android: "launcher_icon"
ios: true ios: true