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

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

View File

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

View File

@@ -715,36 +715,33 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
],
),
),
// Settings cog - hidden in simple mode
if (!context.watch<AppProvider>().isSimpleMode) ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeviceConfigScreen(),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeviceConfigScreen(),
),
);
},
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PacketLogScreen(
bleService: provider.bleService,
),
);
},
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PacketLogScreen(
bleService: provider.bleService,
),
),
);
},
child: Container(
width: 32,
height: 32,
alignment: Alignment.center,
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 {
final MapController _mapController = MapController();
final TileCacheService _tileCache = TileCacheService();
late final TileCacheService _tileCache;
// DO NOT create a new LocationTrackingService instance here
// Use the singleton from AppProvider instead via _locationService getter
final MapMarkerService _markerService = MapMarkerService();
@@ -117,6 +117,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
@override
void initState() {
super.initState();
_tileCache = context.read<TileCacheService>();
// Initialize Slovenian WMS layers with CRS
_slovenianAerialLayer = MapLayer.getSlovenianAerial2024(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
_locationService.onPositionUpdate = _originalLocationCallback;
_mapController.dispose();
_tileCache.dispose();
super.dispose();
}

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import '../../providers/map_provider.dart';
import '../../providers/app_provider.dart';
import 'contact_route_dialog.dart';
import 'room_login_sheet.dart';
import '../common/contact_avatar.dart';
import '../../utils/location_formats.dart';
import '../../utils/toast_logger.dart';
import '../../utils/battery_display_helper.dart';
@@ -147,54 +148,28 @@ class ContactTile extends StatelessWidget {
fontWeight: FontWeight.w800,
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
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
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) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.location_on, size: 12, color: Colors.blue),
const SizedBox(width: 4),
Expanded(
child: Text(
'GPS: ${location.latitude.toStringAsFixed(5)}, ${location.longitude.toStringAsFixed(5)}',
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
),
),
],
const SizedBox(height: 2),
_buildLocationLine(
context,
latitude: location.latitude,
longitude: location.longitude,
distanceText: distanceText,
),
if (contact.type != ContactType.channel ||
distanceText != null) ...[
if (contact.type != ContactType.channel) ...[
const SizedBox(height: 6),
Wrap(
spacing: 8,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (contact.type != ContactType.channel)
_buildRoutePill(context, contact),
if (distanceText != null)
_buildDistancePill(context, distanceText),
],
),
Row(children: [_buildRoutePill(context, contact)]),
],
] else
Padding(
@@ -285,40 +260,17 @@ class ContactTile extends StatelessWidget {
],
Row(
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) ...[
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(
child: Text(
'GPS: ${location.latitude.toStringAsFixed(5)}, ${location.longitude.toStringAsFixed(5)}',
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
child: _buildLocationLine(
context,
latitude: location.latitude,
longitude: location.longitude,
distanceText: distanceText,
telemetryActive: hasTelemetry,
),
),
] 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 SizedBox(width: 4),
Text(
@@ -328,20 +280,9 @@ class ContactTile extends StatelessWidget {
],
],
),
if (contact.type != ContactType.channel ||
distanceText != null) ...[
if (contact.type != ContactType.channel) ...[
const SizedBox(height: 6),
Wrap(
spacing: 8,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (contact.type != ContactType.channel)
_buildRoutePill(context, contact),
if (distanceText != null)
_buildDistancePill(context, distanceText),
],
),
Row(children: [_buildRoutePill(context, contact)]),
],
],
);
@@ -378,51 +319,12 @@ class ContactTile extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Container(
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,
),
),
),
ContactAvatar(contact: contact, radius: 24),
if (contact.isNew)
Positioned(
top: -2,
@@ -480,26 +382,7 @@ class ContactTile extends StatelessWidget {
),
),
const SizedBox(width: 8),
Container(
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,
),
),
),
Text(timeAgoText, style: timeAgoStyle),
if (!isSimpleMode && battery != null) ...[
const SizedBox(width: 6),
_buildBatteryBadge(context, battery),
@@ -656,18 +539,7 @@ class ContactTile extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
CircleAvatar(
backgroundColor: _getTypeColor(contact.type, context),
child: contact.roleEmoji != null
? Text(
contact.roleEmoji!,
style: const TextStyle(fontSize: 24),
)
: Icon(
_getTypeIcon(contact.type),
color: Colors.white,
),
),
ContactAvatar(contact: contact, radius: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
@@ -1116,17 +988,42 @@ class ContactTile extends StatelessWidget {
.where((candidate) => candidate.publicKeyHex != contact.publicKeyHex)
.toList();
final parsedRoute = await ContactRouteDialog.show(
final routeResult = await ContactRouteDialog.show(
context,
contact: contact,
availableContacts: availableContacts,
);
if (parsedRoute == null || !context.mounted) {
if (routeResult == null || !context.mounted) {
return;
}
final previousSignedPathLen = contact.routeSignedPathLen;
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(
contact.publicKey,
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
String _convertToDMS(double lat, double lon) {
String latDir = lat >= 0 ? 'N' : 'S';
@@ -1273,19 +1237,6 @@ class ContactTile extends StatelessWidget {
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) {
switch (type) {
case ContactType.chat:
@@ -1411,6 +1362,7 @@ class ContactTile extends StatelessWidget {
Widget _buildRoutePill(BuildContext context, Contact contact) {
final hasPath = contact.routeHasPath;
final isDirect = !hasPath || contact.routeHopCount <= 0;
final scheme = Theme.of(context).colorScheme;
final textColor = Theme.of(
context,
@@ -1418,9 +1370,7 @@ class ContactTile extends StatelessWidget {
final iconColor = Theme.of(
context,
).textTheme.labelSmall?.color?.withValues(alpha: 0.7);
final label = !hasPath
? AppLocalizations.of(context)!.flood
: contact.routeHopCount <= 0
final label = isDirect
? AppLocalizations.of(context)!.direct
: contact.routeCanonicalText;
@@ -1434,7 +1384,7 @@ class ContactTile extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Icon(
hasPath ? Icons.alt_route : Icons.waves,
isDirect ? Icons.north_east_rounded : Icons.alt_route,
size: 11,
color: iconColor,
),
@@ -1447,9 +1397,7 @@ class ContactTile extends StatelessWidget {
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: textColor,
fontWeight: FontWeight.w600,
fontFamily: hasPath && contact.routeHopCount > 0
? 'monospace'
: null,
fontFamily: !isDirect ? 'monospace' : null,
),
),
),
@@ -1479,7 +1427,7 @@ class ContactTile extends StatelessWidget {
),
const SizedBox(width: 5),
Text(
'${AppLocalizations.of(context)!.distance}: $distanceText',
distanceText,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(
context,