mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
Clarify path_provider override
This commit is contained in:
@@ -489,7 +489,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 104;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -511,7 +511,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 104;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -530,7 +530,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 104;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -547,7 +547,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 104;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -679,7 +679,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 104;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -702,7 +702,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 104;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>104</string>
|
||||
<string>105</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -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 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 classname="fastlane.lanes" name="2: build_app" time="89.53674">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="242.066948">
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="70.83361">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'contacts_provider.dart';
|
||||
enum SensorRefreshState { idle, refreshing, success, timeout, unavailable }
|
||||
|
||||
class SensorsProvider with ChangeNotifier {
|
||||
static const Duration _successStateRetention = Duration(minutes: 1);
|
||||
static const String _watchedSensorsKey = 'watched_sensor_keys';
|
||||
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
||||
static const String _fieldSpanKey = 'sensor_field_spans';
|
||||
@@ -26,6 +27,7 @@ class SensorsProvider with ChangeNotifier {
|
||||
final List<String> _watchedSensorKeys = <String>[];
|
||||
final Map<String, SensorRefreshState> _refreshStates =
|
||||
<String, SensorRefreshState>{};
|
||||
final Map<String, DateTime> _refreshStateUpdatedAt = <String, DateTime>{};
|
||||
final Map<String, Set<String>> _visibleFieldsBySensor =
|
||||
<String, Set<String>>{};
|
||||
final Map<String, Map<String, int>> _fieldSpansBySensor =
|
||||
@@ -189,6 +191,7 @@ class SensorsProvider with ChangeNotifier {
|
||||
Future<void> removeSensor(String publicKeyHex) async {
|
||||
_watchedSensorKeys.remove(publicKeyHex);
|
||||
_refreshStates.remove(publicKeyHex);
|
||||
_refreshStateUpdatedAt.remove(publicKeyHex);
|
||||
_visibleFieldsBySensor.remove(publicKeyHex);
|
||||
_fieldSpansBySensor.remove(publicKeyHex);
|
||||
await _persistWatchedSensors();
|
||||
@@ -246,22 +249,52 @@ class SensorsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
if (contact == null) {
|
||||
_refreshStates[publicKeyHex] = SensorRefreshState.unavailable;
|
||||
notifyListeners();
|
||||
_setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
|
||||
return;
|
||||
}
|
||||
|
||||
_refreshStates[publicKeyHex] = SensorRefreshState.refreshing;
|
||||
notifyListeners();
|
||||
_setRefreshState(publicKeyHex, SensorRefreshState.refreshing);
|
||||
|
||||
final result = await connectionProvider.smartPing(
|
||||
contactPublicKey: contact.publicKey,
|
||||
hasPath: contact.hasPath,
|
||||
);
|
||||
|
||||
_refreshStates[publicKeyHex] = result.success
|
||||
? SensorRefreshState.success
|
||||
: SensorRefreshState.timeout;
|
||||
_setRefreshState(
|
||||
publicKeyHex,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,26 +398,51 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final deviceInfo = context.watch<ConnectionProvider>().deviceInfo;
|
||||
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(
|
||||
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||
children: [
|
||||
// Device Info Card
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
_ConfigHeroCard(
|
||||
title: deviceInfo.selfName ?? deviceInfo.deviceName ?? 'MeshCore',
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.deviceInformation,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_InfoRow(
|
||||
AppLocalizations.of(context)!.bleName,
|
||||
deviceInfo.deviceName ??
|
||||
@@ -467,65 +492,63 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_ConfigSectionCard(
|
||||
title: AppLocalizations.of(context)!.publicInfo,
|
||||
subtitle: AppLocalizations.of(context)!.nameBroadcastInMesh,
|
||||
icon: Icons.public_rounded,
|
||||
trailing: FilledButton.icon(
|
||||
onPressed: _savePublicInfo,
|
||||
icon: const Icon(Icons.save_outlined),
|
||||
label: Text(AppLocalizations.of(context)!.save),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Public Info Section
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.45,
|
||||
),
|
||||
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),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.publicInfo,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filled(
|
||||
onPressed: _savePublicInfo,
|
||||
icon: const Icon(Icons.save),
|
||||
tooltip: AppLocalizations.of(context)!.save,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Mesh Network Name
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!.meshNetworkName,
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: AppLocalizations.of(
|
||||
context,
|
||||
)!.nameBroadcastInMesh,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Telemetry Toggle - Compact version
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.telemetryAndLocationSharing,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
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) {
|
||||
@@ -536,39 +559,33 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// GPS Coordinates (only show if telemetry enabled)
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!.meshNetworkName,
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: AppLocalizations.of(
|
||||
context,
|
||||
)!.nameBroadcastInMesh,
|
||||
),
|
||||
),
|
||||
if (_telemetryEnabled) ...[
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
child: _CompactCoordinateField(
|
||||
controller: _latController,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!.lat,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
signed: true,
|
||||
),
|
||||
label: AppLocalizations.of(context)!.lat,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
child: _CompactCoordinateField(
|
||||
controller: _lonController,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!.lon,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
signed: true,
|
||||
),
|
||||
label: AppLocalizations.of(context)!.lon,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -585,36 +602,45 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_ConfigSectionCard(
|
||||
title: AppLocalizations.of(context)!.radioSettings,
|
||||
subtitle:
|
||||
'${_freqController.text} MHz • SF$_selectedSpreadingFactor • CR$_selectedCodingRate',
|
||||
icon: Icons.settings_input_antenna_rounded,
|
||||
trailing: FilledButton.icon(
|
||||
onPressed: _saveRadioSettings,
|
||||
icon: const Icon(Icons.save_outlined),
|
||||
label: Text(AppLocalizations.of(context)!.save),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Radio Settings Section
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.radioSettings,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
Expanded(
|
||||
child: _RadioMetricTile(
|
||||
label: AppLocalizations.of(context)!.bandwidth,
|
||||
value: _selectedBandwidth,
|
||||
),
|
||||
),
|
||||
IconButton.filled(
|
||||
onPressed: _saveRadioSettings,
|
||||
icon: const Icon(Icons.save),
|
||||
tooltip: AppLocalizations.of(context)!.save,
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _RadioMetricTile(
|
||||
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),
|
||||
|
||||
// LoRa Frequency
|
||||
TextField(
|
||||
controller: _freqController,
|
||||
decoration: InputDecoration(
|
||||
@@ -629,8 +655,6 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Bandwidth
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _selectedBandwidth,
|
||||
decoration: InputDecoration(
|
||||
@@ -652,8 +676,6 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Spreading Factor
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: _selectedSpreadingFactor,
|
||||
decoration: InputDecoration(
|
||||
@@ -677,8 +699,6 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Coding Rate
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: _selectedCodingRate,
|
||||
decoration: InputDecoration(
|
||||
@@ -702,8 +722,6 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// TX Power
|
||||
TextField(
|
||||
controller: _txPowerController,
|
||||
decoration: InputDecoration(
|
||||
@@ -715,11 +733,17 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
|
||||
// Repeat Mode (firmware v9+)
|
||||
if (deviceInfo.clientRepeat != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.45,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Client Repeat Mode'),
|
||||
subtitle:
|
||||
@@ -738,15 +762,14 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
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 {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -10,9 +12,53 @@ import '../providers/contacts_provider.dart';
|
||||
import '../providers/sensors_provider.dart';
|
||||
import '../utils/location_formats.dart';
|
||||
|
||||
class SensorsTab extends StatelessWidget {
|
||||
class SensorsTab extends StatefulWidget {
|
||||
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 {
|
||||
final sensorsProvider = context.read<SensorsProvider>();
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
@@ -588,7 +634,7 @@ class _SensorCard extends StatelessWidget {
|
||||
|
||||
String _formatTelemetryTime(DateTime 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.inHours < 24) return '${diff.inHours}h ago';
|
||||
return '${diff.inDays}d ago';
|
||||
@@ -688,47 +734,31 @@ class _MetricTile extends StatelessWidget {
|
||||
final location = data.mapLocation;
|
||||
if (location == null) return;
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: SizedBox(
|
||||
height: 420,
|
||||
child: Column(
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (pageContext) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.label,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(data.label),
|
||||
Text(
|
||||
data.value,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: Theme.of(pageContext).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (data.secondaryValue != null)
|
||||
Text(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: Text(
|
||||
data.secondaryValue!,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
style: Theme.of(pageContext).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -763,9 +793,10 @@ class _MetricTile extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -969,7 +969,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.22"
|
||||
path_provider_foundation:
|
||||
dependency: "direct overridden"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
@@ -1582,5 +1582,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.0 <4.0.0"
|
||||
flutter: ">=3.38.1"
|
||||
dart: ">=3.10.3 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
||||
@@ -138,9 +138,6 @@ dev_dependencies:
|
||||
flutter_launcher_icons: "^0.14.4"
|
||||
fake_async: ^1.3.3
|
||||
|
||||
dependency_overrides:
|
||||
path_provider_foundation: 2.6.0
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: "launcher_icon"
|
||||
ios: true
|
||||
|
||||
Reference in New Issue
Block a user