Remove unused path_provider entry

This commit is contained in:
Janez T
2026-03-07 20:23:09 +01:00
parent 79c8664d04
commit 3433eae3a6
11 changed files with 251 additions and 272 deletions

View File

@@ -65,9 +65,6 @@ PODS:
- ObjectBox (= 4.4.1) - ObjectBox (= 4.4.1)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0): - permission_handler_apple (9.3.0):
- Flutter - Flutter
- record_ios (1.2.0): - record_ios (1.2.0):
@@ -105,7 +102,6 @@ DEPENDENCIES:
- nsd_ios (from `.symlinks/plugins/nsd_ios/ios`) - nsd_ios (from `.symlinks/plugins/nsd_ios/ios`)
- objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`) - objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_ios (from `.symlinks/plugins/record_ios/ios`) - record_ios (from `.symlinks/plugins/record_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
@@ -153,8 +149,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/objectbox_flutter_libs/ios" :path: ".symlinks/plugins/objectbox_flutter_libs/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple: permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
record_ios: record_ios:
@@ -182,14 +176,13 @@ SPEC CHECKSUMS:
flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1 flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e
ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757 ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757
objectbox_flutter_libs: 09b1dec1b4cd27bf1a5f9bae7ccaa7e43588bf31 objectbox_flutter_libs: 09b1dec1b4cd27bf1a5f9bae7ccaa7e43588bf31
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844 record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf

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 = 100; CURRENT_PROJECT_VERSION = 101;
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 = 100; CURRENT_PROJECT_VERSION = 101;
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 = 100; CURRENT_PROJECT_VERSION = 101;
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 = 100; CURRENT_PROJECT_VERSION = 101;
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 = 100; CURRENT_PROJECT_VERSION = 101;
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 = 100; CURRENT_PROJECT_VERSION = 101;
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>100</string> <string>101</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000199"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000228">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.436347"> <testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.473911">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="49.579697"> <testcase classname="fastlane.lanes" name="2: build_app" time="95.078238">
</testcase> </testcase>

View File

@@ -231,10 +231,10 @@ extension ContactLocalization on Contact {
} }
String get routeSummary { String get routeSummary {
if (routeIsUnknown || !routeHasPath) { if (routeIsUnknown) {
return 'Flood/Unknown'; return 'Unknown';
} }
if (routeHopCount == 0) { if (!routeHasPath || routeHopCount == 0) {
return 'Direct'; return 'Direct';
} }
return '$routeHopCount hop${routeHopCount == 1 ? '' : 's'} via $routeHashSize-byte hashes'; return '$routeHopCount hop${routeHopCount == 1 ? '' : 's'} via $routeHashSize-byte hashes';

View File

@@ -2015,6 +2015,7 @@ class ConnectionProvider with ChangeNotifier {
} }
try { try {
_error = null;
await _activeService.resetPath(contactPublicKey); await _activeService.resetPath(contactPublicKey);
} catch (e) { } catch (e) {
_error = 'Failed to reset path: $e'; _error = 'Failed to reset path: $e';
@@ -2034,6 +2035,7 @@ class ConnectionProvider with ChangeNotifier {
} }
try { try {
_error = null;
final updatedContact = contact.copyWith( final updatedContact = contact.copyWith(
outPathLen: signedEncodedPathLen, outPathLen: signedEncodedPathLen,
outPath: Uint8List.fromList(paddedPathBytes), outPath: Uint8List.fromList(paddedPathBytes),

View File

@@ -715,36 +715,33 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
], ],
), ),
), ),
// Settings cog - hidden in simple mode const SizedBox(width: 8),
if (!context.watch<AppProvider>().isSimpleMode) ...[ GestureDetector(
const SizedBox(width: 8), onTap: () {
GestureDetector( Navigator.push(
onTap: () { context,
Navigator.push( MaterialPageRoute(
context, builder: (context) => const DeviceConfigScreen(),
MaterialPageRoute( ),
builder: (context) => const DeviceConfigScreen(), );
},
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PacketLogScreen(
bleService: provider.bleService,
), ),
); ),
}, );
onLongPress: () { },
Navigator.push( child: Container(
context, width: 32,
MaterialPageRoute( height: 32,
builder: (context) => PacketLogScreen( alignment: Alignment.center,
bleService: provider.bleService, child: const Icon(Icons.settings, size: 18),
),
),
);
},
child: Container(
width: 32,
height: 32,
alignment: Alignment.center,
child: const Icon(Icons.settings, size: 18),
),
), ),
], ),
], ],
), ),
), ),

View File

@@ -59,7 +59,7 @@ class MapTab extends StatefulWidget {
class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin { class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
final MapController _mapController = MapController(); final MapController _mapController = MapController();
final TileCacheService _tileCache = TileCacheService(); late final TileCacheService _tileCache;
// DO NOT create a new LocationTrackingService instance here // DO NOT create a new LocationTrackingService instance here
// Use the singleton from AppProvider instead via _locationService getter // Use the singleton from AppProvider instead via _locationService getter
final MapMarkerService _markerService = MapMarkerService(); final MapMarkerService _markerService = MapMarkerService();
@@ -117,6 +117,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tileCache = context.read<TileCacheService>();
// Initialize Slovenian WMS layers with CRS // Initialize Slovenian WMS layers with CRS
_slovenianAerialLayer = MapLayer.getSlovenianAerial2024(slovenianCrs); _slovenianAerialLayer = MapLayer.getSlovenianAerial2024(slovenianCrs);
_dtk25Layer = MapLayer.getDTK25(slovenianCrs); _dtk25Layer = MapLayer.getDTK25(slovenianCrs);
@@ -446,7 +447,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// Restore the original callback instead of setting to null // Restore the original callback instead of setting to null
_locationService.onPositionUpdate = _originalLocationCallback; _locationService.onPositionUpdate = _originalLocationCallback;
_mapController.dispose(); _mapController.dispose();
_tileCache.dispose();
super.dispose(); super.dispose();
} }

View File

@@ -10,47 +10,66 @@ class TileCacheService {
// Global flag to ensure ObjectBox is only initialized once // Global flag to ensure ObjectBox is only initialized once
static bool _objectBoxInitialized = false; static bool _objectBoxInitialized = false;
static final _initLock = <String, Future<void>>{}; static Future<void>? _objectBoxInitialization;
late final FMTCStore _store; Future<void>? _initializeFuture;
FMTCStore? _store;
bool _isInitialized = false; bool _isInitialized = false;
bool _isDownloading = false; bool _isDownloading = false;
Future<void> initialize() async { Future<void> initialize() async {
if (_isInitialized) return; if (_isInitialized) return;
_initializeFuture ??= _initializeInternal();
await _initializeFuture;
}
// Ensure we only initialize ObjectBox once globally Future<void> _initializeInternal() async {
if (!_objectBoxInitialized) {
// Use a lock to prevent concurrent initialization attempts
final initFuture = _initLock.putIfAbsent('objectbox', () async {
try {
await FMTCObjectBoxBackend().initialise();
_objectBoxInitialized = true;
} catch (e) {
// Already initialized or error - that's okay
_objectBoxInitialized = true;
}
});
await initFuture;
}
try { try {
_store = FMTCStore(_storeName); await _ensureObjectBoxInitialized();
await _store.manage.create();
_isInitialized = true; final store = FMTCStore(_storeName);
} catch (e) { try {
// Store might already exist await store.manage.create();
_store = FMTCStore(_storeName); } catch (_) {
// The store may already exist from a prior initialization.
}
_store = store;
_isInitialized = true; _isInitialized = true;
} catch (_) {
_initializeFuture = null;
rethrow;
} }
} }
FMTCTileProvider getTileProvider(MapLayer layer) { Future<void> _ensureObjectBoxInitialized() async {
if (!_isInitialized) { if (_objectBoxInitialized) return;
_objectBoxInitialization ??= () async {
try {
await FMTCObjectBoxBackend().initialise();
} catch (_) {
// Treat repeated backend initialization as a no-op.
} finally {
_objectBoxInitialized = true;
}
}();
await _objectBoxInitialization;
}
FMTCStore _requireStore() {
final store = _store;
if (!_isInitialized || store == null) {
throw StateError( throw StateError(
'TileCacheService not initialized. Call initialize() first.', 'TileCacheService not initialized. Call initialize() first.',
); );
} }
return store;
}
TileProvider getTileProvider(MapLayer layer) {
_requireStore();
return FMTCTileProvider( return FMTCTileProvider(
stores: {_storeName: BrowseStoreStrategy.readUpdateCreate}, stores: {_storeName: BrowseStoreStrategy.readUpdateCreate},
loadingStrategy: BrowseLoadingStrategy.cacheFirst, loadingStrategy: BrowseLoadingStrategy.cacheFirst,
@@ -60,12 +79,8 @@ class TileCacheService {
/// Get tile provider for WMS layers with caching support /// Get tile provider for WMS layers with caching support
/// WMS layers require special handling because they use WMSTileLayerOptions /// WMS layers require special handling because they use WMSTileLayerOptions
FMTCTileProvider getTileProviderForWms(MapLayer layer) { TileProvider getTileProviderForWms(MapLayer layer) {
if (!_isInitialized) { _requireStore();
throw StateError(
'TileCacheService not initialized. Call initialize() first.',
);
}
if (!layer.isWms) { if (!layer.isWms) {
throw ArgumentError('Layer must be a WMS layer'); throw ArgumentError('Layer must be a WMS layer');
} }
@@ -97,6 +112,7 @@ class TileCacheService {
} }
_isDownloading = true; _isDownloading = true;
final store = _requireStore();
try { try {
final region = RectangleRegion(bounds); final region = RectangleRegion(bounds);
@@ -107,7 +123,7 @@ class TileCacheService {
options: TileLayer(urlTemplate: layer.urlTemplate), options: TileLayer(urlTemplate: layer.urlTemplate),
); );
final download = _store.download.startForeground(region: downloadable); final download = store.download.startForeground(region: downloadable);
await for (final progress in download.downloadProgress) { await for (final progress in download.downloadProgress) {
if (onProgress != null && progress.maxTilesCount > 0) { if (onProgress != null && progress.maxTilesCount > 0) {
@@ -127,24 +143,25 @@ class TileCacheService {
Future<void> cancelDownload() async { Future<void> cancelDownload() async {
if (!_isInitialized) return; if (!_isInitialized) return;
await _store.download.cancel(); await _requireStore().download.cancel();
} }
Future<void> clearCache() async { Future<void> clearCache() async {
if (!_isInitialized) return; if (!_isInitialized) return;
await _store.manage.delete(); final store = _requireStore();
await _store.manage.create(); await store.manage.delete();
await store.manage.create();
} }
Future<int> getCachedTileCount() async { Future<int> getCachedTileCount() async {
if (!_isInitialized) return 0; if (!_isInitialized) return 0;
final stats = await _store.stats.length; final stats = await _requireStore().stats.length;
return stats; return stats;
} }
Future<double> getCacheSizeMB() async { Future<double> getCacheSizeMB() async {
if (!_isInitialized) return 0.0; if (!_isInitialized) return 0.0;
final stats = await _store.stats.size; final stats = await _requireStore().stats.size;
return stats / (1024 * 1024); return stats / (1024 * 1024);
} }
@@ -162,8 +179,9 @@ class TileCacheService {
Future<Map<String, dynamic>> getStoreStats() async { Future<Map<String, dynamic>> getStoreStats() async {
if (!_isInitialized) return {}; if (!_isInitialized) return {};
final length = await _store.stats.length; final store = _requireStore();
final size = await _store.stats.all.then((a) => a.size); final length = await store.stats.length;
final size = await store.stats.all.then((a) => a.size);
return { return {
'tileCount': length, 'tileCount': length,

View File

@@ -3,6 +3,18 @@ import 'package:flutter/material.dart';
import '../../models/contact.dart'; import '../../models/contact.dart';
import '../../services/route_hash_preferences.dart'; import '../../services/route_hash_preferences.dart';
class ContactRouteDialogResult {
final ParsedContactRoute? route;
final bool shouldClear;
const ContactRouteDialogResult._({this.route, required this.shouldClear});
const ContactRouteDialogResult.set(ParsedContactRoute route)
: this._(route: route, shouldClear: false);
const ContactRouteDialogResult.clear() : this._(shouldClear: true);
}
class ContactRouteDialog extends StatefulWidget { class ContactRouteDialog extends StatefulWidget {
final Contact contact; final Contact contact;
final List<Contact> availableContacts; final List<Contact> availableContacts;
@@ -13,12 +25,12 @@ class ContactRouteDialog extends StatefulWidget {
required this.availableContacts, required this.availableContacts,
}); });
static Future<ParsedContactRoute?> show( static Future<ContactRouteDialogResult?> show(
BuildContext context, { BuildContext context, {
required Contact contact, required Contact contact,
required List<Contact> availableContacts, required List<Contact> availableContacts,
}) { }) {
return showModalBottomSheet<ParsedContactRoute>( return showModalBottomSheet<ContactRouteDialogResult>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
showDragHandle: true, showDragHandle: true,
@@ -212,11 +224,20 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
if (widget.contact.routeHasPath)
TextButton(
onPressed: () => Navigator.of(
context,
).pop(const ContactRouteDialogResult.clear()),
child: const Text('Clear Route'),
),
const Spacer(), const Spacer(),
FilledButton( FilledButton(
onPressed: _parsedRoute == null onPressed: _parsedRoute == null
? null ? null
: () => Navigator.of(context).pop(_parsedRoute), : () => Navigator.of(
context,
).pop(ContactRouteDialogResult.set(_parsedRoute!)),
child: const Text('Set Route'), child: const Text('Set Route'),
), ),
], ],

View File

@@ -11,6 +11,7 @@ import '../../providers/map_provider.dart';
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import 'contact_route_dialog.dart'; import 'contact_route_dialog.dart';
import 'room_login_sheet.dart'; import 'room_login_sheet.dart';
import '../common/contact_avatar.dart';
import '../../utils/location_formats.dart'; import '../../utils/location_formats.dart';
import '../../utils/toast_logger.dart'; import '../../utils/toast_logger.dart';
import '../../utils/battery_display_helper.dart'; import '../../utils/battery_display_helper.dart';
@@ -147,54 +148,28 @@ class ContactTile extends StatelessWidget {
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
letterSpacing: -0.3, letterSpacing: -0.3,
); );
final timeAgoText = _getLocalizedTimeSinceLastSeen(context);
final timeAgoStyle = Theme.of(context).textTheme.labelSmall?.copyWith(
color: contact.isRecentlySeen
? colorScheme.primary
: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
);
final subtitleWidget = isSimpleMode final subtitleWidget = isSimpleMode
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 2),
Row(
children: [
Icon(
Icons.access_time,
size: 12,
color: contact.isRecentlySeen ? Colors.green : Colors.grey,
),
const SizedBox(width: 4),
Text(
'${AppLocalizations.of(context)!.lastSeen}: ${_getLocalizedTimeSinceLastSeen(context)}',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
if (location != null) ...[ if (location != null) ...[
const SizedBox(height: 4), const SizedBox(height: 2),
Row( _buildLocationLine(
children: [ context,
const Icon(Icons.location_on, size: 12, color: Colors.blue), latitude: location.latitude,
const SizedBox(width: 4), longitude: location.longitude,
Expanded( distanceText: distanceText,
child: Text(
'GPS: ${location.latitude.toStringAsFixed(5)}, ${location.longitude.toStringAsFixed(5)}',
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
),
),
],
), ),
if (contact.type != ContactType.channel || if (contact.type != ContactType.channel) ...[
distanceText != null) ...[
const SizedBox(height: 6), const SizedBox(height: 6),
Wrap( Row(children: [_buildRoutePill(context, contact)]),
spacing: 8,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (contact.type != ContactType.channel)
_buildRoutePill(context, contact),
if (distanceText != null)
_buildDistancePill(context, distanceText),
],
),
], ],
] else ] else
Padding( Padding(
@@ -285,40 +260,17 @@ class ContactTile extends StatelessWidget {
], ],
Row( Row(
children: [ children: [
Icon(
Icons.access_time,
size: 12,
color: contact.isRecentlySeen ? Colors.green : Colors.grey,
),
const SizedBox(width: 4),
Text(
_getLocalizedTimeSinceLastSeen(context),
style: Theme.of(context).textTheme.labelSmall,
),
if (location != null) ...[ if (location != null) ...[
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
if (hasTelemetry)
const Icon(Icons.sensors, size: 12, color: Colors.green)
else
const Icon(
Icons.sensors_off,
size: 12,
color: Colors.grey,
),
const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: _buildLocationLine(
'GPS: ${location.latitude.toStringAsFixed(5)}, ${location.longitude.toStringAsFixed(5)}', context,
style: Theme.of(context).textTheme.labelSmall, latitude: location.latitude,
overflow: TextOverflow.ellipsis, longitude: location.longitude,
distanceText: distanceText,
telemetryActive: hasTelemetry,
), ),
), ),
] else ...[ ] else ...[
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
const Icon(Icons.sensors_off, size: 12, color: Colors.grey), const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
@@ -328,20 +280,9 @@ class ContactTile extends StatelessWidget {
], ],
], ],
), ),
if (contact.type != ContactType.channel || if (contact.type != ContactType.channel) ...[
distanceText != null) ...[
const SizedBox(height: 6), const SizedBox(height: 6),
Wrap( Row(children: [_buildRoutePill(context, contact)]),
spacing: 8,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (contact.type != ContactType.channel)
_buildRoutePill(context, contact),
if (distanceText != null)
_buildDistancePill(context, distanceText),
],
),
], ],
], ],
); );
@@ -378,51 +319,12 @@ class ContactTile extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Stack( Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Container( ContactAvatar(contact: contact, radius: 24),
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
_getTypeColor(contact.type, context),
_getTypeColor(
contact.type,
context,
).withValues(alpha: 0.72),
],
),
boxShadow: [
BoxShadow(
color: _getTypeColor(
contact.type,
context,
).withValues(alpha: 0.22),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Center(
child: contact.roleEmoji != null
? Text(
contact.roleEmoji!,
style: const TextStyle(fontSize: 22),
)
: Icon(
_getTypeIcon(contact.type),
color: Colors.white,
size: 20,
),
),
),
if (contact.isNew) if (contact.isNew)
Positioned( Positioned(
top: -2, top: -2,
@@ -480,26 +382,7 @@ class ContactTile extends StatelessWidget {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Text(timeAgoText, style: timeAgoStyle),
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 3,
),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest
.withValues(alpha: 0.75),
borderRadius: BorderRadius.circular(999),
),
child: Text(
contact.type.displayName,
style: Theme.of(context).textTheme.labelSmall
?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
),
),
),
if (!isSimpleMode && battery != null) ...[ if (!isSimpleMode && battery != null) ...[
const SizedBox(width: 6), const SizedBox(width: 6),
_buildBatteryBadge(context, battery), _buildBatteryBadge(context, battery),
@@ -656,18 +539,7 @@ class ContactTile extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row( child: Row(
children: [ children: [
CircleAvatar( ContactAvatar(contact: contact, radius: 20),
backgroundColor: _getTypeColor(contact.type, context),
child: contact.roleEmoji != null
? Text(
contact.roleEmoji!,
style: const TextStyle(fontSize: 24),
)
: Icon(
_getTypeIcon(contact.type),
color: Colors.white,
),
),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
@@ -1116,17 +988,42 @@ class ContactTile extends StatelessWidget {
.where((candidate) => candidate.publicKeyHex != contact.publicKeyHex) .where((candidate) => candidate.publicKeyHex != contact.publicKeyHex)
.toList(); .toList();
final parsedRoute = await ContactRouteDialog.show( final routeResult = await ContactRouteDialog.show(
context, context,
contact: contact, contact: contact,
availableContacts: availableContacts, availableContacts: availableContacts,
); );
if (parsedRoute == null || !context.mounted) { if (routeResult == null || !context.mounted) {
return; return;
} }
final previousSignedPathLen = contact.routeSignedPathLen; final previousSignedPathLen = contact.routeSignedPathLen;
final previousPathBytes = Uint8List.fromList(contact.outPath); final previousPathBytes = Uint8List.fromList(contact.outPath);
if (routeResult.shouldClear) {
contactsProvider.resetContactRouteLocal(contact.publicKey);
await connectionProvider.resetPath(contact.publicKey);
final error = connectionProvider.error;
if (error != null) {
contactsProvider.setContactRouteLocal(
contact.publicKey,
signedEncodedPathLen: previousSignedPathLen,
paddedPathBytes: previousPathBytes,
);
if (context.mounted) {
ToastLogger.error(context, 'Failed to clear route: $error');
}
return;
}
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Route cleared')));
}
return;
}
final parsedRoute = routeResult.route!;
contactsProvider.setContactRouteLocal( contactsProvider.setContactRouteLocal(
contact.publicKey, contact.publicKey,
signedEncodedPathLen: parsedRoute.signedEncodedPathLen, signedEncodedPathLen: parsedRoute.signedEncodedPathLen,
@@ -1217,6 +1114,73 @@ class ContactTile extends StatelessWidget {
); );
} }
Widget _buildLocationMeta(
BuildContext context,
double latitude,
double longitude, {
bool telemetryActive = true,
}) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 18,
height: 18,
decoration: BoxDecoration(
color: telemetryActive
? colorScheme.primary.withValues(alpha: 0.12)
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(999),
),
child: Icon(
telemetryActive
? Icons.navigation_rounded
: Icons.location_searching,
size: 11,
color: telemetryActive
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 6),
Text(
'${latitude.toStringAsFixed(5)}, ${longitude.toStringAsFixed(5)}',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
fontFamily: 'monospace',
color: colorScheme.onSurfaceVariant,
letterSpacing: 0.1,
),
overflow: TextOverflow.ellipsis,
),
],
);
}
Widget _buildLocationLine(
BuildContext context, {
required double latitude,
required double longitude,
String? distanceText,
bool telemetryActive = true,
}) {
return Wrap(
spacing: 8,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
_buildLocationMeta(
context,
latitude,
longitude,
telemetryActive: telemetryActive,
),
if (distanceText != null) _buildDistancePill(context, distanceText),
],
);
}
/// Convert to Degrees Minutes Seconds (DMS) format /// Convert to Degrees Minutes Seconds (DMS) format
String _convertToDMS(double lat, double lon) { String _convertToDMS(double lat, double lon) {
String latDir = lat >= 0 ? 'N' : 'S'; String latDir = lat >= 0 ? 'N' : 'S';
@@ -1273,19 +1237,6 @@ class ContactTile extends StatelessWidget {
return '$zone$letter (approximate)'; return '$zone$letter (approximate)';
} }
IconData _getTypeIcon(ContactType type) {
switch (type) {
case ContactType.chat:
return Icons.person;
case ContactType.repeater:
return Icons.router;
case ContactType.room:
return Icons.tag;
default:
return Icons.help;
}
}
Color _getTypeColor(ContactType type, BuildContext context) { Color _getTypeColor(ContactType type, BuildContext context) {
switch (type) { switch (type) {
case ContactType.chat: case ContactType.chat:
@@ -1411,6 +1362,7 @@ class ContactTile extends StatelessWidget {
Widget _buildRoutePill(BuildContext context, Contact contact) { Widget _buildRoutePill(BuildContext context, Contact contact) {
final hasPath = contact.routeHasPath; final hasPath = contact.routeHasPath;
final isDirect = !hasPath || contact.routeHopCount <= 0;
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final textColor = Theme.of( final textColor = Theme.of(
context, context,
@@ -1418,9 +1370,7 @@ class ContactTile extends StatelessWidget {
final iconColor = Theme.of( final iconColor = Theme.of(
context, context,
).textTheme.labelSmall?.color?.withValues(alpha: 0.7); ).textTheme.labelSmall?.color?.withValues(alpha: 0.7);
final label = !hasPath final label = isDirect
? AppLocalizations.of(context)!.flood
: contact.routeHopCount <= 0
? AppLocalizations.of(context)!.direct ? AppLocalizations.of(context)!.direct
: contact.routeCanonicalText; : contact.routeCanonicalText;
@@ -1434,7 +1384,7 @@ class ContactTile extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(
hasPath ? Icons.alt_route : Icons.waves, isDirect ? Icons.north_east_rounded : Icons.alt_route,
size: 11, size: 11,
color: iconColor, color: iconColor,
), ),
@@ -1447,9 +1397,7 @@ class ContactTile extends StatelessWidget {
style: Theme.of(context).textTheme.labelSmall?.copyWith( style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: textColor, color: textColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontFamily: hasPath && contact.routeHopCount > 0 fontFamily: !isDirect ? 'monospace' : null,
? 'monospace'
: null,
), ),
), ),
), ),
@@ -1479,7 +1427,7 @@ class ContactTile extends StatelessWidget {
), ),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
'${AppLocalizations.of(context)!.distance}: $distanceText', distanceText,
style: Theme.of(context).textTheme.labelSmall?.copyWith( style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of( color: Theme.of(
context, context,