Compare commits

..

1 Commits

Author SHA1 Message Date
Janez T
5cc81bef65 feat: Show self sensor default 2026-03-20 19:08:24 +01:00
5 changed files with 173 additions and 133 deletions

View File

@@ -193,6 +193,9 @@ jobs:
- name: Install dependencies
run: flutter pub get
- name: Install macOS native build tools
run: brew install automake libtool
- name: Build macOS release
run: flutter build macos --release

View File

@@ -5,22 +5,22 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000814">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000217">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.353323">
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.503497">
</testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="115.148883">
<testcase classname="fastlane.lanes" name="2: build_app" time="103.12875">
</testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="204.902574">
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="58490.472877">
</testcase>

View File

@@ -474,6 +474,21 @@ class SensorsProvider with ChangeNotifier {
bool isWatched(String publicKeyHex) =>
_watchedSensorKeys.contains(publicKeyHex);
Contact? selfContact(
ContactsProvider contactsProvider,
ConnectionProvider connectionProvider,
) {
final selfKey = connectionProvider.deviceInfo.publicKey;
if (selfKey == null || selfKey.isEmpty) {
return null;
}
final existing = contactsProvider.findContactByKey(
Uint8List.fromList(selfKey),
);
return existing ?? _buildSelfCandidate(connectionProvider);
}
Future<void> addSensor(Contact contact) async {
if (!contact.isChat && !contact.isRepeater && !contact.isSensor) {
return;
@@ -549,17 +564,65 @@ class SensorsProvider with ChangeNotifier {
notifyListeners();
}
List<Contact> availableCandidates(ContactsProvider contactsProvider) {
List<Contact> availableCandidates(
ContactsProvider contactsProvider, {
ConnectionProvider? connectionProvider,
}) {
final candidates = <Contact>[
...contactsProvider.chatContacts,
...contactsProvider.repeaters,
...contactsProvider.sensorContacts,
];
// Add "myself" (own device) as a candidate if it has a public key
if (connectionProvider != null) {
final self = selfContact(contactsProvider, connectionProvider);
if (self != null &&
!candidates.any((c) => c.publicKeyHex == self.publicKeyHex)) {
candidates.insert(0, self);
}
}
candidates.removeWhere((contact) => isWatched(contact.publicKeyHex));
candidates.sort((a, b) => b.lastSeenTime.compareTo(a.lastSeenTime));
return candidates;
}
List<String> displaySensorKeys({
required ContactsProvider contactsProvider,
required ConnectionProvider connectionProvider,
}) {
if (_watchedSensorKeys.isNotEmpty) {
return List<String>.unmodifiable(_watchedSensorKeys);
}
final self = selfContact(contactsProvider, connectionProvider);
if (self == null) {
return const <String>[];
}
return <String>[self.publicKeyHex];
}
Contact? contactForDisplay(
String publicKeyHex, {
required ContactsProvider contactsProvider,
required ConnectionProvider connectionProvider,
}) {
for (final entry in contactsProvider.contacts) {
if (entry.publicKeyHex == publicKeyHex) {
return entry;
}
}
final self = selfContact(contactsProvider, connectionProvider);
if (self?.publicKeyHex == publicKeyHex) {
return self;
}
return null;
}
Future<void> refreshAll({
required ContactsProvider contactsProvider,
required ConnectionProvider connectionProvider,
@@ -597,13 +660,11 @@ class SensorsProvider with ChangeNotifier {
_lastRefreshAttemptAt[publicKeyHex] = requestedAt ?? DateTime.now();
Contact? contact;
for (final entry in contactsProvider.contacts) {
if (entry.publicKeyHex == publicKeyHex) {
contact = entry;
break;
}
}
final contact = contactForDisplay(
publicKeyHex,
contactsProvider: contactsProvider,
connectionProvider: connectionProvider,
);
if (contact == null) {
_setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
@@ -686,4 +747,25 @@ class SensorsProvider with ChangeNotifier {
_refreshStateUpdatedAt[publicKeyHex] = DateTime.now();
notifyListeners();
}
Contact? _buildSelfCandidate(ConnectionProvider connectionProvider) {
final deviceInfo = connectionProvider.deviceInfo;
final selfKey = deviceInfo.publicKey;
if (selfKey == null || selfKey.isEmpty) {
return null;
}
return Contact(
publicKey: Uint8List.fromList(selfKey),
type: ContactType.chat,
flags: 0,
outPathLen: 0,
outPath: Uint8List(64),
advName: deviceInfo.selfName ?? 'My Device',
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
advLat: deviceInfo.advLat ?? 0,
advLon: deviceInfo.advLon ?? 0,
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
}
}

View File

@@ -100,7 +100,10 @@ class _SensorsTabState extends State<SensorsTab> {
Future<void> _showAddSensorSheet(BuildContext context) async {
final sensorsProvider = context.read<SensorsProvider>();
final contactsProvider = context.read<ContactsProvider>();
final candidates = sensorsProvider.availableCandidates(contactsProvider);
final candidates = sensorsProvider.availableCandidates(
contactsProvider,
connectionProvider: context.read<ConnectionProvider>(),
);
await showModalBottomSheet<void>(
context: context,
@@ -127,7 +130,11 @@ class _SensorsTabState extends State<SensorsTab> {
'Add sensor node',
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(AppLocalizations.of(context)!.pickARelayOrNodeToWatchInSensors),
subtitle: Text(
AppLocalizations.of(
context,
)!.pickARelayOrNodeToWatchInSensors,
),
),
...candidates.map(
(contact) => ListTile(
@@ -275,58 +282,72 @@ class _SensorsTabState extends State<SensorsTab> {
onPressed: () => _showAddSensorSheet(context),
child: const Icon(Icons.add),
),
body: Consumer2<SensorsProvider, ContactsProvider>(
builder: (context, sensorsProvider, contactsProvider, child) {
final watchedKeys = sensorsProvider.watchedSensorKeys;
body: Consumer3<SensorsProvider, ContactsProvider, ConnectionProvider>(
builder:
(
context,
sensorsProvider,
contactsProvider,
connectionProvider,
child,
) {
final displayKeys = sensorsProvider.displaySensorKeys(
contactsProvider: contactsProvider,
connectionProvider: connectionProvider,
);
final hasPersistedSensors =
sensorsProvider.watchedSensorKeys.isNotEmpty;
return RefreshIndicator(
onRefresh: () => _refreshAll(context),
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
children: [
if (watchedKeys.isEmpty)
const _EmptySensorsState()
else
...watchedKeys.map((key) {
Contact? contact;
for (final entry in contactsProvider.contacts) {
if (entry.publicKeyHex == key) {
contact = entry;
break;
}
}
final availableFieldKeys = sensorMetricKeysFor(contact);
final visibleFields = sensorsProvider
.effectiveVisibleFieldsFor(key, availableFieldKeys);
return SensorTelemetryCard(
contact: contact,
state: sensorsProvider.stateFor(key),
visibleFields: visibleFields,
fieldOrder: sensorsProvider.metricOrderFor(
key,
availableFieldKeys,
),
labelOverrides: sensorsProvider.labelOverridesFor(key),
fieldSpans: {
for (final field in visibleFields)
field: sensorsProvider.fieldSpanFor(key, field),
},
onRemove: () async {
await sensorsProvider.removeSensor(key);
},
onCustomize: () =>
_showMetricSelector(context, key, contact),
onRefresh: () => sensorsProvider.refreshSensor(
publicKeyHex: key,
contactsProvider: contactsProvider,
connectionProvider: context.read<ConnectionProvider>(),
),
);
}),
],
),
);
},
return RefreshIndicator(
onRefresh: () => _refreshAll(context),
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
children: [
if (displayKeys.isEmpty)
const _EmptySensorsState()
else
...displayKeys.map((key) {
final contact = sensorsProvider.contactForDisplay(
key,
contactsProvider: contactsProvider,
connectionProvider: connectionProvider,
);
final availableFieldKeys = sensorMetricKeysFor(contact);
final visibleFields = sensorsProvider
.effectiveVisibleFieldsFor(key, availableFieldKeys);
return SensorTelemetryCard(
contact: contact,
state: sensorsProvider.stateFor(key),
visibleFields: visibleFields,
fieldOrder: sensorsProvider.metricOrderFor(
key,
availableFieldKeys,
),
labelOverrides: sensorsProvider.labelOverridesFor(
key,
),
fieldSpans: {
for (final field in visibleFields)
field: sensorsProvider.fieldSpanFor(key, field),
},
onRemove: hasPersistedSensors
? () async {
await sensorsProvider.removeSensor(key);
}
: null,
onCustomize: () =>
_showMetricSelector(context, key, contact),
onRefresh: () => sensorsProvider.refreshSensor(
publicKeyHex: key,
contactsProvider: contactsProvider,
connectionProvider: connectionProvider,
),
);
}),
],
),
);
},
),
);
}
@@ -876,55 +897,8 @@ class _EmptySensorsState extends StatefulWidget {
}
class _EmptySensorsStateState extends State<_EmptySensorsState> {
bool _discoveryTriggered = false;
bool _discoveryInProgress = false;
@override
void initState() {
super.initState();
// Auto-trigger sensor discovery when the empty state is shown
WidgetsBinding.instance.addPostFrameCallback((_) {
_autoDiscover();
});
}
Future<void> _autoDiscover() async {
if (_discoveryTriggered || !mounted) return;
final connectionProvider = context.read<ConnectionProvider>();
if (!connectionProvider.deviceInfo.isConnected) return;
_discoveryTriggered = true;
setState(() => _discoveryInProgress = true);
try {
await connectionProvider.discoverNodeType(advertType: 4);
} finally {
if (mounted) setState(() => _discoveryInProgress = false);
}
}
Future<void> _discoverSensors() async {
if (_discoveryInProgress || !mounted) return;
final connectionProvider = context.read<ConnectionProvider>();
if (!connectionProvider.deviceInfo.isConnected) return;
setState(() => _discoveryInProgress = true);
try {
await connectionProvider.discoverNodeType(advertType: 4);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context)!.sensorDiscoverySent)),
);
}
} finally {
if (mounted) setState(() => _discoveryInProgress = false);
}
}
@override
Widget build(BuildContext context) {
final isConnected =
context.watch<ConnectionProvider>().deviceInfo.isConnected;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 64),
child: Column(
@@ -951,29 +925,10 @@ class _EmptySensorsStateState extends State<_EmptySensorsState> {
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
'Use + to add discovered relays or nodes. Pull down to refresh telemetry after adding them.',
'Use + to add discovered relays or nodes. Your device will appear here automatically when available.',
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: isConnected && !_discoveryInProgress
? _discoverSensors
: null,
icon: _discoveryInProgress
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.sensors_outlined),
label: Text(_discoveryInProgress
? 'Discovering...'
: 'Discover Sensors'),
),
],
),
);

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 2026.0319.1+35
version: 2026.0320.1+36
environment:
sdk: ^3.9.2