From 05153c9ceb0308b8cafb6793301a20a9e147f12a Mon Sep 17 00:00:00 2001 From: Janez T Date: Sun, 8 Mar 2026 09:30:30 +0100 Subject: [PATCH] Clarify path_provider override --- ios/Runner.xcodeproj/project.pbxproj | 12 +- ios/Runner/Info.plist | 2 +- ios/fastlane/report.xml | 11 +- lib/providers/sensors_provider.dart | 47 ++- lib/screens/device_config_screen.dart | 577 +++++++++++++++++++------- lib/screens/sensors_tab.dart | 121 ++++-- pubspec.lock | 6 +- pubspec.yaml | 3 - 8 files changed, 564 insertions(+), 215 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e8eb266..8d47f3c 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index baee051..a45b3dd 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -43,7 +43,7 @@ CFBundleSignature ???? CFBundleVersion - 104 + 105 LSRequiresIPhoneOS ITSAppUsesNonExemptEncryption diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index c88930a..451471c 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,17 @@ - + - + - - - - - - + diff --git a/lib/providers/sensors_provider.dart b/lib/providers/sensors_provider.dart index a558bdf..fdc42b4 100644 --- a/lib/providers/sensors_provider.dart +++ b/lib/providers/sensors_provider.dart @@ -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 _watchedSensorKeys = []; final Map _refreshStates = {}; + final Map _refreshStateUpdatedAt = {}; final Map> _visibleFieldsBySensor = >{}; final Map> _fieldSpansBySensor = @@ -189,6 +191,7 @@ class SensorsProvider with ChangeNotifier { Future 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 = []; + + 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(); } } diff --git a/lib/screens/device_config_screen.dart b/lib/screens/device_config_screen.dart index c072cfb..1a13b84 100644 --- a/lib/screens/device_config_screen.dart +++ b/lib/screens/device_config_screen.dart @@ -398,26 +398,51 @@ class _DeviceConfigScreenState extends State { Widget build(BuildContext context) { final deviceInfo = context.watch().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), - children: [ - // Device Info Card - Card( - child: Padding( - padding: const EdgeInsets.all(16), + body: SafeArea( + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + children: [ + _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,42 +492,75 @@ class _DeviceConfigScreenState extends State { ], ), ), - ), - - const SizedBox(height: 24), - - // Public Info Section - Card( - child: Padding( - padding: const EdgeInsets.all(16), + 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), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.publicInfo, - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, + 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, ), - ), - 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(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of( + context, + )!.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), - - // Mesh Network Name + const SizedBox(height: 18), TextField( controller: _nameController, decoration: InputDecoration( @@ -513,62 +571,21 @@ class _DeviceConfigScreenState extends State { )!.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) ...[ - 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 { ], ), ), - ), - - const SizedBox(height: 24), - - // Radio Settings Section - Card( - child: Padding( - padding: const EdgeInsets.all(16), + 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), + ), 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 { ), ), const SizedBox(height: 16), - - // Bandwidth DropdownButtonFormField( initialValue: _selectedBandwidth, decoration: InputDecoration( @@ -652,8 +676,6 @@ class _DeviceConfigScreenState extends State { }, ), const SizedBox(height: 16), - - // Spreading Factor DropdownButtonFormField( initialValue: _selectedSpreadingFactor, decoration: InputDecoration( @@ -677,8 +699,6 @@ class _DeviceConfigScreenState extends State { }, ), const SizedBox(height: 16), - - // Coding Rate DropdownButtonFormField( initialValue: _selectedCodingRate, decoration: InputDecoration( @@ -702,8 +722,6 @@ class _DeviceConfigScreenState extends State { }, ), const SizedBox(height: 16), - - // TX Power TextField( controller: _txPowerController, decoration: InputDecoration( @@ -715,37 +733,42 @@ class _DeviceConfigScreenState extends State { ), keyboardType: TextInputType.number, ), - - // Repeat Mode (firmware v9+) if (deviceInfo.clientRepeat != null) ...[ - const SizedBox(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Client Repeat Mode'), - subtitle: - deviceInfo.allowedRepeatFreqRanges != null && - deviceInfo.allowedRepeatFreqRanges!.isNotEmpty - ? Text( - '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: 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: + deviceInfo.allowedRepeatFreqRanges != null && + deviceInfo.allowedRepeatFreqRanges!.isNotEmpty + ? Text( + '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 { } } +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; diff --git a/lib/screens/sensors_tab.dart b/lib/screens/sensors_tab.dart index c674ad6..f07c252 100644 --- a/lib/screens/sensors_tab.dart +++ b/lib/screens/sensors_tab.dart @@ -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 createState() => _SensorsTabState(); +} + +class _SensorsTabState extends State { + 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().clearExpiredRefreshStates(); + setState(() {}); + _minuteTicker = Timer.periodic(const Duration(minutes: 1), (_) { + if (!mounted) return; + context.read().clearExpiredRefreshStates(); + setState(() {}); + }); + }); + } + Future _showAddSensorSheet(BuildContext context) async { final sensorsProvider = context.read(); final contactsProvider = context.read(); @@ -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,49 +734,33 @@ class _MetricTile extends StatelessWidget { final location = data.mapLocation; if (location == null) return; - await showDialog( - 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( + builder: (pageContext) { + return Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(data.label), + Text( + data.value, + style: Theme.of(pageContext).textTheme.bodySmall, + ), + ], + ), + ), + body: 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.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), - ), - ], + if (data.secondaryValue != null) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Text( + data.secondaryValue!, + style: Theme.of(pageContext).textTheme.bodyMedium, + ), ), - ), Expanded( child: flutter_map.FlutterMap( options: flutter_map.MapOptions( @@ -763,9 +793,10 @@ class _MetricTile extends StatelessWidget { ), ], ), - ), - ); - }, + ); + }, + fullscreenDialog: true, + ), ); } diff --git a/pubspec.lock b/pubspec.lock index 08b4afd..e9a438d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index e0ed892..1cc0deb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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