mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
fix: Tighten device settings layout
This commit is contained in:
@@ -2553,12 +2553,10 @@ class AppProvider with ChangeNotifier {
|
||||
try {
|
||||
_isReconnectSyncInProgress = true;
|
||||
_hasCompletedConnectionBootstrap = false;
|
||||
// Initialize contacts provider with device public key to exclude self
|
||||
// If already initialized (from early load), this will just filter out self-contact
|
||||
// This must happen before getContacts to ensure proper filtering
|
||||
await contactsProvider.initialize(
|
||||
await contactsProvider.prepareForDeviceContactSync(
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
channelsProvider.prepareForDeviceSync();
|
||||
|
||||
// Note: Device clock is automatically synced during connection in MeshCoreBleService
|
||||
// No need to sync it again here
|
||||
@@ -2634,10 +2632,14 @@ class AppProvider with ChangeNotifier {
|
||||
'🔄 [AppProvider] Device reconnected - syncing contacts and missed messages',
|
||||
);
|
||||
|
||||
await contactsProvider.initialize(
|
||||
await contactsProvider.prepareForDeviceContactSync(
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
channelsProvider.prepareForDeviceSync();
|
||||
await connectionProvider.getContacts();
|
||||
await connectionProvider.syncChannels(
|
||||
maxChannels: connectionProvider.deviceInfo.maxChannels,
|
||||
);
|
||||
|
||||
final messageCount = await connectionProvider.syncAllMessages(
|
||||
force: true,
|
||||
@@ -3642,6 +3644,7 @@ class AppProvider with ChangeNotifier {
|
||||
await connectionProvider.getContacts();
|
||||
|
||||
// Sync all channels so refresh reflects the full device state.
|
||||
channelsProvider.prepareForDeviceSync();
|
||||
await connectionProvider.syncChannels(
|
||||
maxChannels: connectionProvider.deviceInfo.maxChannels,
|
||||
);
|
||||
|
||||
@@ -7,7 +7,8 @@ class ChannelsProvider with ChangeNotifier {
|
||||
int _selectedChannelIndex = 0; // Default to public channel
|
||||
|
||||
/// Get all channels
|
||||
List<Channel> get channels => _channels.values.toList()..sort((a, b) => a.index.compareTo(b.index));
|
||||
List<Channel> get channels =>
|
||||
_channels.values.toList()..sort((a, b) => a.index.compareTo(b.index));
|
||||
|
||||
/// Get a specific channel by index
|
||||
Channel? getChannel(int index) => _channels[index];
|
||||
@@ -54,12 +55,12 @@ class ChannelsProvider with ChangeNotifier {
|
||||
void removeChannel(int index) {
|
||||
if (_channels.containsKey(index)) {
|
||||
_channels.remove(index);
|
||||
|
||||
|
||||
// If the deleted channel was selected, switch to public channel
|
||||
if (_selectedChannelIndex == index) {
|
||||
_selectedChannelIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -96,6 +97,13 @@ class ChannelsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear runtime channel state before a live device sync begins.
|
||||
void prepareForDeviceSync() {
|
||||
_channels.clear();
|
||||
_selectedChannelIndex = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Check if channels have been loaded
|
||||
bool get hasChannels => _channels.isNotEmpty;
|
||||
|
||||
|
||||
@@ -547,6 +547,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
manufacturerModel: deviceInfo['manufacturerModel'] as String?,
|
||||
semanticVersion: deviceInfo['semanticVersion'] as String?,
|
||||
clientRepeat: deviceInfo['clientRepeat'] as bool?,
|
||||
pathHashMode: deviceInfo['pathHashMode'] as int?,
|
||||
supportsSpectrumScan: deviceInfo['supportsSpectrumScan'] as bool?,
|
||||
spectrumScanMinKhz: deviceInfo['spectrumScanMinKhz'] as int?,
|
||||
spectrumScanMaxKhz: deviceInfo['spectrumScanMaxKhz'] as int?,
|
||||
@@ -563,6 +564,9 @@ class ConnectionProvider with ChangeNotifier {
|
||||
publicKey: selfInfo['publicKey'] as Uint8List?,
|
||||
advLat: selfInfo['advLat'] as int?,
|
||||
advLon: selfInfo['advLon'] as int?,
|
||||
multiAcks: selfInfo['multiAcks'] as int?,
|
||||
advertLocPolicy: selfInfo['advertLocPolicy'] as int?,
|
||||
telemetryModes: selfInfo['telemetryModes'] as int?,
|
||||
manualAddContacts: selfInfo['manualAddContacts'] as bool?,
|
||||
radioFreq: selfInfo['radioFreq'] as int?,
|
||||
radioBw: selfInfo['radioBw'] as int?,
|
||||
@@ -605,6 +609,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: config['autoAddRoomServers'] as bool?,
|
||||
autoAddSensors: config['autoAddSensors'] as bool?,
|
||||
autoAddOverwriteOldest: config['autoAddOverwriteOldest'] as bool?,
|
||||
autoAddMaxHops: config['autoAddMaxHops'] as int?,
|
||||
);
|
||||
notifyListeners();
|
||||
};
|
||||
@@ -1853,11 +1858,13 @@ class ConnectionProvider with ChangeNotifier {
|
||||
return pendingPing;
|
||||
}
|
||||
|
||||
final future = _runSmartPing(
|
||||
contactPublicKey: contactPublicKey,
|
||||
hasPath: hasPath,
|
||||
onRetryWithFlooding: onRetryWithFlooding,
|
||||
);
|
||||
final future = _isSelfPublicKey(contactPublicKey)
|
||||
? _runSelfTelemetryPing(contactPublicKey)
|
||||
: _runSmartPing(
|
||||
contactPublicKey: contactPublicKey,
|
||||
hasPath: hasPath,
|
||||
onRetryWithFlooding: onRetryWithFlooding,
|
||||
);
|
||||
_pendingSmartPings[pingKey] = future;
|
||||
notifyListeners();
|
||||
|
||||
@@ -1944,10 +1951,53 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<PingResult> _runSelfTelemetryPing(Uint8List devicePublicKey) async {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return PingResult(success: false, usedFlooding: false, timedOut: true);
|
||||
}
|
||||
|
||||
try {
|
||||
final pingFuture = _pingTracker.trackPing(
|
||||
publicKey: devicePublicKey,
|
||||
wasDirectAttempt: true,
|
||||
);
|
||||
|
||||
// Firmware treats a 4-byte telemetry request as "self telemetry".
|
||||
await _activeService.requestTelemetry(Uint8List(0), zeroHop: true);
|
||||
|
||||
final gotResponse = await pingFuture;
|
||||
return PingResult(
|
||||
success: gotResponse,
|
||||
usedFlooding: false,
|
||||
timedOut: !gotResponse,
|
||||
);
|
||||
} catch (e) {
|
||||
_error = 'Failed to request self telemetry: $e';
|
||||
notifyListeners();
|
||||
return PingResult(success: false, usedFlooding: false, timedOut: true);
|
||||
}
|
||||
}
|
||||
|
||||
String _publicKeyToHex(Uint8List publicKey) {
|
||||
return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||
}
|
||||
|
||||
bool _isSelfPublicKey(Uint8List publicKey) {
|
||||
final selfKey = _deviceInfo.publicKey;
|
||||
if (selfKey == null || selfKey.length != publicKey.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < publicKey.length; i++) {
|
||||
if (selfKey[i] != publicKey[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Send binary request to contact (modern replacement for requestTelemetry)
|
||||
///
|
||||
/// Supports multiple request types:
|
||||
@@ -2292,6 +2342,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: config['autoAddRoomServers'] as bool?,
|
||||
autoAddSensors: config['autoAddSensors'] as bool?,
|
||||
autoAddOverwriteOldest: config['autoAddOverwriteOldest'] as bool?,
|
||||
autoAddMaxHops: config['autoAddMaxHops'] as int?,
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
@@ -2303,6 +2354,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: null,
|
||||
autoAddSensors: null,
|
||||
autoAddOverwriteOldest: null,
|
||||
autoAddMaxHops: null,
|
||||
);
|
||||
notifyListeners();
|
||||
return;
|
||||
@@ -2322,6 +2374,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
required bool autoAddRoomServers,
|
||||
required bool autoAddSensors,
|
||||
required bool overwriteOldest,
|
||||
int maxHops = 0,
|
||||
}) async {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
@@ -2336,6 +2389,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: autoAddRoomServers,
|
||||
autoAddSensors: autoAddSensors,
|
||||
overwriteOldest: overwriteOldest,
|
||||
maxHops: maxHops,
|
||||
);
|
||||
_deviceInfo = _deviceInfo.copyWith(
|
||||
autoAddUsers: autoAddUsers,
|
||||
@@ -2343,6 +2397,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
autoAddRoomServers: autoAddRoomServers,
|
||||
autoAddSensors: autoAddSensors,
|
||||
autoAddOverwriteOldest: overwriteOldest,
|
||||
autoAddMaxHops: maxHops,
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
@@ -2351,6 +2406,23 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setPathHashMode(int mode) async {
|
||||
if (!_activeService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _activeService.setPathHashMode(mode);
|
||||
_deviceInfo = _deviceInfo.copyWith(pathHashMode: mode);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = 'Failed to set path hash mode: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Export a contact as a meshcore:// share URL.
|
||||
/// Pass null to export self.
|
||||
Future<String?> exportContactUrl(Uint8List? publicKey) async {
|
||||
@@ -2440,14 +2512,8 @@ class ConnectionProvider with ChangeNotifier {
|
||||
Future<void> requestSelfTelemetry() async {
|
||||
if (!_activeService.isConnected) return;
|
||||
try {
|
||||
// Request own telemetry by sending telemetry req with zero-length key
|
||||
final deviceKey = _deviceInfo.publicKey;
|
||||
if (deviceKey != null) {
|
||||
await _activeService.requestTelemetry(
|
||||
Uint8List.fromList(deviceKey),
|
||||
zeroHop: true,
|
||||
);
|
||||
}
|
||||
// Firmware expects a 4-byte CMD_SEND_TELEMETRY_REQ frame for "self".
|
||||
await _activeService.requestTelemetry(Uint8List(0), zeroHop: true);
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [Provider] requestSelfTelemetry failed: $e');
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@ class ContactsProvider with ChangeNotifier {
|
||||
bool _isPersistingPendingAdverts = false;
|
||||
bool _persistPendingAdvertsRequested = false;
|
||||
String? _storageNamespace;
|
||||
String? _selfPublicKeyHex;
|
||||
ContactTelemetry? _selfTelemetry;
|
||||
|
||||
// Add default public channel on initialization
|
||||
ContactsProvider()
|
||||
@@ -162,6 +164,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
Uint8List? devicePublicKey,
|
||||
}) async {
|
||||
_storageNamespace = namespace;
|
||||
_setSelfDevicePublicKey(devicePublicKey);
|
||||
await _loadFromStorage(force: true, devicePublicKey: devicePublicKey);
|
||||
}
|
||||
|
||||
@@ -253,6 +256,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
/// Initialize and load persisted contacts
|
||||
/// [devicePublicKey] - device's own public key to exclude from loaded contacts
|
||||
Future<void> initialize({Uint8List? devicePublicKey}) async {
|
||||
_setSelfDevicePublicKey(devicePublicKey);
|
||||
if (_isInitialized) {
|
||||
// If already initialized (from early load), just filter out self-contact
|
||||
if (devicePublicKey != null) {
|
||||
@@ -267,6 +271,36 @@ class ContactsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear runtime contact state before a live device contact sync begins.
|
||||
///
|
||||
/// This intentionally does not touch persisted storage. It keeps any saved
|
||||
/// contact groups for the active profile, but removes stale in-memory device
|
||||
/// contacts and discovery state so a newly connected device starts from an
|
||||
/// empty list while sync is in progress.
|
||||
Future<void> prepareForDeviceContactSync({Uint8List? devicePublicKey}) async {
|
||||
_setSelfDevicePublicKey(devicePublicKey);
|
||||
_selfTelemetry = null;
|
||||
if (!_isInitialized) {
|
||||
final storedGroups = await _storageService.loadContactGroups(
|
||||
namespace: _storageNamespace,
|
||||
);
|
||||
_savedContactGroups
|
||||
..clear()
|
||||
..addAll(storedGroups);
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'🧹 [ContactsProvider] Clearing runtime contacts before device sync',
|
||||
);
|
||||
_contacts.clear();
|
||||
_pendingAdverts.clear();
|
||||
_estimatedLocations.clear();
|
||||
_rssiObservations.clear();
|
||||
_ensurePublicChannelExists();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Remove self-contact from loaded contacts (called after BLE connection established)
|
||||
void _removeSelfContact(Uint8List devicePublicKey) {
|
||||
final selfKeyHex = devicePublicKey
|
||||
@@ -347,6 +381,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
List<Contact> get contacts => _contacts.values.toList();
|
||||
ContactTelemetry? get selfTelemetry => _selfTelemetry;
|
||||
List<Contact> get favouriteContacts =>
|
||||
_contacts.values.where((c) => c.isFavourite).toList();
|
||||
List<SavedContactGroup> get savedContactGroups =>
|
||||
@@ -537,9 +572,11 @@ class ContactsProvider with ChangeNotifier {
|
||||
// Replace existing observation from the same repeater, or add new
|
||||
final repeaterKey =
|
||||
'${observation.repeaterLocation.latitude},${observation.repeaterLocation.longitude}';
|
||||
observations.removeWhere((o) =>
|
||||
'${o.repeaterLocation.latitude},${o.repeaterLocation.longitude}' ==
|
||||
repeaterKey);
|
||||
observations.removeWhere(
|
||||
(o) =>
|
||||
'${o.repeaterLocation.latitude},${o.repeaterLocation.longitude}' ==
|
||||
repeaterKey,
|
||||
);
|
||||
observations.add(observation);
|
||||
|
||||
// Keep at most 8 observations (most recent per repeater)
|
||||
@@ -956,13 +993,22 @@ class ContactsProvider with ChangeNotifier {
|
||||
|
||||
// Find contact by public key prefix
|
||||
final contact = _findContactByPrefix(publicKeyPrefix);
|
||||
if (contact == null) {
|
||||
final isSelfTelemetry =
|
||||
contact == null && _matchesSelfPrefix(publicKeyPrefix);
|
||||
if (contact == null && !isSelfTelemetry) {
|
||||
debugPrint(' ❌ Contact not found for this prefix');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(' ✅ Found contact: ${contact.advName}');
|
||||
debugPrint(' Old telemetry timestamp: ${contact.telemetry?.timestamp}');
|
||||
if (isSelfTelemetry) {
|
||||
debugPrint(' ✅ Matched self telemetry response');
|
||||
debugPrint(
|
||||
' Old self telemetry timestamp: ${_selfTelemetry?.timestamp}',
|
||||
);
|
||||
} else {
|
||||
debugPrint(' ✅ Found contact: ${contact!.advName}');
|
||||
debugPrint(' Old telemetry timestamp: ${contact.telemetry?.timestamp}');
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse Cayenne LPP data
|
||||
@@ -985,7 +1031,9 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
final previousTelemetry = contact.telemetry;
|
||||
final previousTelemetry = isSelfTelemetry
|
||||
? _selfTelemetry
|
||||
: contact!.telemetry;
|
||||
|
||||
final mergedTelemetry = _mergeTelemetryForContact(
|
||||
existingTelemetry: previousTelemetry,
|
||||
@@ -1012,25 +1060,34 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
if (isSelfTelemetry) {
|
||||
_selfTelemetry = telemetry;
|
||||
notifyListeners();
|
||||
debugPrint(' ✅ Updated self telemetry');
|
||||
return;
|
||||
}
|
||||
|
||||
final resolvedContact = contact!;
|
||||
|
||||
// Update contact with new telemetry AND last seen time
|
||||
// lastAdvert is Unix timestamp in seconds
|
||||
final currentTimestamp = (DateTime.now().millisecondsSinceEpoch / 1000)
|
||||
.round();
|
||||
debugPrint(' Old lastAdvert: ${contact.lastAdvert}');
|
||||
debugPrint(' Old lastAdvert: ${resolvedContact.lastAdvert}');
|
||||
debugPrint(' New lastAdvert: $currentTimestamp');
|
||||
|
||||
final persistedGps = _getValidGpsOrNull(telemetry.gpsLocation);
|
||||
final updatedContact = contact.copyWith(
|
||||
final updatedContact = resolvedContact.copyWith(
|
||||
telemetry: telemetry,
|
||||
lastAdvert: currentTimestamp, // Update last seen time
|
||||
advLat: persistedGps != null
|
||||
? _coordinateToAdvertMicrodegrees(persistedGps.latitude)
|
||||
: contact.advLat,
|
||||
: resolvedContact.advLat,
|
||||
advLon: persistedGps != null
|
||||
? _coordinateToAdvertMicrodegrees(persistedGps.longitude)
|
||||
: contact.advLon,
|
||||
: resolvedContact.advLon,
|
||||
);
|
||||
_contacts[contact.publicKeyHex] = updatedContact;
|
||||
_contacts[resolvedContact.publicKeyHex] = updatedContact;
|
||||
debugPrint(' ✅ Updated contact in map (with new lastAdvert)');
|
||||
|
||||
_persistContacts();
|
||||
@@ -1237,6 +1294,36 @@ class ContactsProvider with ChangeNotifier {
|
||||
return _contacts[keyHex];
|
||||
}
|
||||
|
||||
void _setSelfDevicePublicKey(Uint8List? devicePublicKey) {
|
||||
final nextKeyHex = _publicKeyHexOrNull(devicePublicKey);
|
||||
if (_selfPublicKeyHex != nextKeyHex) {
|
||||
_selfTelemetry = null;
|
||||
}
|
||||
_selfPublicKeyHex = nextKeyHex;
|
||||
}
|
||||
|
||||
String? _publicKeyHexOrNull(Uint8List? publicKey) {
|
||||
if (publicKey == null || publicKey.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||
}
|
||||
|
||||
bool _matchesSelfPrefix(Uint8List prefix) {
|
||||
final selfKeyHex = _selfPublicKeyHex;
|
||||
if (selfKeyHex == null || prefix.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final takeLen = prefix.length < 6 ? prefix.length : 6;
|
||||
final prefixHex = prefix
|
||||
.sublist(0, takeLen)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join('');
|
||||
return selfKeyHex.startsWith(prefixHex);
|
||||
}
|
||||
|
||||
/// Clear a contact's learned path locally so the UI and next send both
|
||||
/// prefer flood routing until the radio reports a fresh route.
|
||||
void markPathUnhealthy(Uint8List publicKey) {
|
||||
@@ -1642,6 +1729,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
_pendingAdverts.clear();
|
||||
_estimatedLocations.clear();
|
||||
_rssiObservations.clear();
|
||||
_selfTelemetry = null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _pendingAdvertToJson(PendingAdvert advert) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import 'helpers/raw_session_retransmit.dart';
|
||||
import '../utils/image_message_parser.dart';
|
||||
|
||||
@@ -75,6 +76,8 @@ class ImageProvider with ChangeNotifier {
|
||||
_restore();
|
||||
}
|
||||
|
||||
String _scopedStorageKey() => ProfileStorageScope.scopedKey(_storageKey);
|
||||
|
||||
// ── Accessors ────────────────────────────────────────────────────────────
|
||||
|
||||
ImageSession? session(String sessionId) => _sessions[sessionId];
|
||||
@@ -292,18 +295,22 @@ class ImageProvider with ChangeNotifier {
|
||||
// ── Persistence ──────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> clearAll() async {
|
||||
_sessions.clear();
|
||||
_outgoing.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
_resetInMemoryState();
|
||||
notifyListeners();
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_storageKey);
|
||||
await prefs.remove(_scopedStorageKey());
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ImageProvider] Failed to clear storage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
_resetInMemoryState();
|
||||
await _restore();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _evictExpiredOutgoing() {
|
||||
final now = DateTime.now();
|
||||
_outgoing.removeWhere((_, s) => now.difference(s.cachedAt) > _outgoingTtl);
|
||||
@@ -343,7 +350,7 @@ class ImageProvider with ChangeNotifier {
|
||||
)
|
||||
.toList(),
|
||||
};
|
||||
await prefs.setString(_storageKey, jsonEncode(payload));
|
||||
await prefs.setString(_scopedStorageKey(), jsonEncode(payload));
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ImageProvider] Failed to persist: $e');
|
||||
}
|
||||
@@ -352,7 +359,7 @@ class ImageProvider with ChangeNotifier {
|
||||
Future<void> _restore() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_storageKey);
|
||||
final raw = prefs.getString(_scopedStorageKey());
|
||||
if (raw == null || raw.isEmpty) return;
|
||||
|
||||
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||
@@ -428,6 +435,12 @@ class ImageProvider with ChangeNotifier {
|
||||
debugPrint('❌ [ImageProvider] Failed to restore: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _resetInMemoryState() {
|
||||
_sessions.clear();
|
||||
_outgoing.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _OutgoingSession {
|
||||
|
||||
@@ -486,7 +486,14 @@ class SensorsProvider with ChangeNotifier {
|
||||
final existing = contactsProvider.findContactByKey(
|
||||
Uint8List.fromList(selfKey),
|
||||
);
|
||||
return existing ?? _buildSelfCandidate(connectionProvider);
|
||||
final selfTelemetry = contactsProvider.selfTelemetry;
|
||||
if (existing != null) {
|
||||
return selfTelemetry == null
|
||||
? existing
|
||||
: existing.copyWith(telemetry: selfTelemetry);
|
||||
}
|
||||
|
||||
return _buildSelfCandidate(connectionProvider, telemetry: selfTelemetry);
|
||||
}
|
||||
|
||||
Future<void> addSensor(Contact contact) async {
|
||||
@@ -564,6 +571,31 @@ class SensorsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> reorderSensors(int oldIndex, int newIndex) async {
|
||||
if (_watchedSensorKeys.length < 2) {
|
||||
return;
|
||||
}
|
||||
if (oldIndex < 0 ||
|
||||
oldIndex >= _watchedSensorKeys.length ||
|
||||
newIndex < 0 ||
|
||||
newIndex > _watchedSensorKeys.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var targetIndex = newIndex;
|
||||
if (oldIndex < targetIndex) {
|
||||
targetIndex -= 1;
|
||||
}
|
||||
if (oldIndex == targetIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
final movedKey = _watchedSensorKeys.removeAt(oldIndex);
|
||||
_watchedSensorKeys.insert(targetIndex, movedKey);
|
||||
await _persistWatchedSensors();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Contact> availableCandidates(
|
||||
ContactsProvider contactsProvider, {
|
||||
ConnectionProvider? connectionProvider,
|
||||
@@ -748,7 +780,10 @@ class SensorsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Contact? _buildSelfCandidate(ConnectionProvider connectionProvider) {
|
||||
Contact? _buildSelfCandidate(
|
||||
ConnectionProvider connectionProvider, {
|
||||
ContactTelemetry? telemetry,
|
||||
}) {
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
final selfKey = deviceInfo.publicKey;
|
||||
if (selfKey == null || selfKey.isEmpty) {
|
||||
@@ -766,6 +801,7 @@ class SensorsProvider with ChangeNotifier {
|
||||
advLat: deviceInfo.advLat ?? 0,
|
||||
advLon: deviceInfo.advLon ?? 0,
|
||||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
telemetry: telemetry,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import 'helpers/raw_session_retransmit.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
import '../services/voice_codec_service.dart';
|
||||
@@ -92,6 +93,9 @@ class VoiceProvider with ChangeNotifier {
|
||||
_restorePersistedVoiceData();
|
||||
}
|
||||
|
||||
String _storageKey() =>
|
||||
ProfileStorageScope.scopedKey(_voiceSessionsStorageKey);
|
||||
|
||||
// ── Session accessors ────────────────────────────────────────────────────
|
||||
|
||||
VoiceSession? session(String sessionId) => _sessions[sessionId];
|
||||
@@ -329,19 +333,22 @@ class VoiceProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> clearStoredVoiceData() async {
|
||||
_sessions.clear();
|
||||
_outgoingSessions.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
_playingSessionId = null;
|
||||
await _resetInMemoryState();
|
||||
notifyListeners();
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_voiceSessionsStorageKey);
|
||||
await prefs.remove(_storageKey());
|
||||
} catch (e) {
|
||||
debugPrint('❌ [VoiceProvider] Failed to clear stored voice data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
await _resetInMemoryState();
|
||||
await _restorePersistedVoiceData();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _persistVoiceData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -365,7 +372,7 @@ class VoiceProvider with ChangeNotifier {
|
||||
)
|
||||
.toList(),
|
||||
};
|
||||
await prefs.setString(_voiceSessionsStorageKey, jsonEncode(payload));
|
||||
await prefs.setString(_storageKey(), jsonEncode(payload));
|
||||
} catch (e) {
|
||||
debugPrint('❌ [VoiceProvider] Failed to persist voice data: $e');
|
||||
}
|
||||
@@ -374,7 +381,7 @@ class VoiceProvider with ChangeNotifier {
|
||||
Future<void> _restorePersistedVoiceData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_voiceSessionsStorageKey);
|
||||
final raw = prefs.getString(_storageKey());
|
||||
if (raw == null || raw.isEmpty) return;
|
||||
|
||||
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||
@@ -437,6 +444,16 @@ class VoiceProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resetInMemoryState() async {
|
||||
if (_playingSessionId != null || _player.isPlaying) {
|
||||
await _player.stop();
|
||||
}
|
||||
_sessions.clear();
|
||||
_outgoingSessions.clear();
|
||||
_ignoredIncomingSessions.clear();
|
||||
_playingSessionId = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_playerEventsSub.cancel();
|
||||
|
||||
Reference in New Issue
Block a user