fix: init tabs advert quick add

ref:
This commit is contained in:
Janez T
2026-02-28 11:23:11 +01:00
parent baf49d27d8
commit 6bd7517180
32 changed files with 850 additions and 11558 deletions

View File

@@ -20,6 +20,7 @@ class ContactsTab extends StatefulWidget {
class _ContactsTabState extends State<ContactsTab> {
Position? _currentPosition;
final Set<String> _resolvingAdvertKeys = <String>{};
@override
void initState() {
@@ -59,6 +60,25 @@ class _ContactsTabState extends State<ContactsTab> {
await _getCurrentLocation();
}
Future<void> _handleResolveAdvert(PendingAdvert advert) async {
final keyHex = advert.publicKeyHex;
if (_resolvingAdvertKeys.contains(keyHex)) return;
setState(() {
_resolvingAdvertKeys.add(keyHex);
});
try {
await context.read<ConnectionProvider>().getContact(advert.publicKey);
} finally {
if (mounted) {
setState(() {
_resolvingAdvertKeys.remove(keyHex);
});
}
}
}
/// Calculate distance between two points in meters
double _calculateDistanceInMeters(
double lat1,
@@ -90,6 +110,15 @@ class _ContactsTabState extends State<ContactsTab> {
}
}
String _formatRelativeTime(BuildContext context, DateTime when) {
final l10n = AppLocalizations.of(context)!;
final diff = DateTime.now().difference(when);
if (diff.inMinutes < 1) return l10n.justNow;
if (diff.inMinutes < 60) return l10n.minutesAgo(diff.inMinutes);
if (diff.inHours < 24) return l10n.hoursAgo(diff.inHours);
return l10n.daysAgo(diff.inDays);
}
/// Show the add channel dialog
Future<void> _showAddChannelDialog(BuildContext context) async {
final l10n = AppLocalizations.of(context)!;
@@ -140,11 +169,14 @@ class _ContactsTabState extends State<ContactsTab> {
final repeaters = contactsProvider.repeaters;
final rooms = contactsProvider.rooms;
final channels = contactsProvider.channels;
final pendingAdverts = contactsProvider.pendingAdverts;
// Check if there are any displayable contacts (excluding channels)
final hasDisplayableContacts = chatContacts.isNotEmpty ||
final hasDisplayableContacts =
chatContacts.isNotEmpty ||
repeaters.isNotEmpty ||
rooms.isNotEmpty;
rooms.isNotEmpty ||
pendingAdverts.isNotEmpty;
if (!hasDisplayableContacts) {
return Center(
@@ -177,6 +209,27 @@ class _ContactsTabState extends State<ContactsTab> {
child: ListView(
padding: const EdgeInsets.all(8),
children: [
// Pending adverts (public key only; quick resolve)
if (pendingAdverts.isNotEmpty) ...[
_SectionHeader(
title: l10n.pending,
count: pendingAdverts.length,
icon: Icons.person_add_alt_1,
),
...pendingAdverts.map(
(advert) => _PendingAdvertTile(
advert: advert,
subtitle:
'${l10n.publicKey}: ${advert.publicKeyHex}\n${l10n.lastSeen}: ${_formatRelativeTime(context, advert.receivedAt)}',
isResolving: _resolvingAdvertKeys.contains(
advert.publicKeyHex,
),
onResolve: () => _handleResolveAdvert(advert),
),
),
const Divider(height: 32),
],
// Team Members (Chat contacts)
if (chatContacts.isNotEmpty) ...[
_SectionHeader(
@@ -280,6 +333,46 @@ class _ContactsTabState extends State<ContactsTab> {
}
}
class _PendingAdvertTile extends StatelessWidget {
final PendingAdvert advert;
final String subtitle;
final bool isResolving;
final VoidCallback onResolve;
const _PendingAdvertTile({
required this.advert,
required this.subtitle,
required this.isResolving,
required this.onResolve,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: const CircleAvatar(child: Icon(Icons.campaign_outlined)),
title: Text(
advert.shortDisplayKey,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(subtitle),
trailing: isResolving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: IconButton(
icon: const Icon(Icons.person_add_alt_1),
tooltip: 'Quick add',
onPressed: onResolve,
),
),
);
}
}
class _SectionHeader extends StatelessWidget {
final String title;
final int count;

View File

@@ -53,6 +53,8 @@ class _HomeScreenState extends State<HomeScreen>
@override
void initState() {
super.initState();
// Initialize synchronously so first build always has a valid controller.
_initTabController();
_loadMapEnabledAndInitTabs();
_loadRxTxPreference();
@@ -67,12 +69,9 @@ class _HomeScreenState extends State<HomeScreen>
Future<void> _loadMapEnabledAndInitTabs() async {
final prefs = await SharedPreferences.getInstance();
final mapEnabled = prefs.getBool('map_enabled') ?? true;
if (mapEnabled != _isMapEnabled) {
_isMapEnabled = mapEnabled;
}
_initTabController();
if (mounted) {
setState(() {});
if (!mounted) return;
if (_isMapEnabled != mapEnabled) {
_updateTabController(mapEnabled);
}
}
@@ -286,7 +285,8 @@ class _HomeScreenState extends State<HomeScreen>
}
// Determine if we should hide the UI (only in fullscreen on map tab)
final shouldHideUI = _isMapEnabled && _isMapFullscreen && _currentIndex == 2;
final shouldHideUI =
_isMapEnabled && _isMapFullscreen && _currentIndex == 2;
return Scaffold(
appBar: shouldHideUI
@@ -296,7 +296,9 @@ class _HomeScreenState extends State<HomeScreen>
actions: [
Consumer<ConnectionProvider>(
builder: (context, provider, child) {
final isConnected = provider.deviceInfo.isConnected || provider.isSseClientConnected;
final isConnected =
provider.deviceInfo.isConnected ||
provider.isSseClientConnected;
if (isConnected) {
return IconButton(
onPressed: () async {
@@ -540,11 +542,14 @@ class _HomeScreenState extends State<HomeScreen>
color: isSseConnected
? Colors.green
: (deviceInfo.signalRssi != null
? BatteryDisplayHelper.getSignalColor(deviceInfo.signalRssi!)
: Colors.grey),
? BatteryDisplayHelper.getSignalColor(
deviceInfo.signalRssi!,
)
: Colors.grey),
size: 13,
),
if (isBleConnected && deviceInfo.signalRssi != null) ...[
if (isBleConnected &&
deviceInfo.signalRssi != null) ...[
const SizedBox(width: 3),
Text(
'${deviceInfo.signalRssi}',
@@ -569,7 +574,9 @@ class _HomeScreenState extends State<HomeScreen>
if (deviceInfo.batteryPercent != null) ...[
const SizedBox(width: 8),
Icon(
BatteryDisplayHelper.getBatteryIcon(deviceInfo.batteryPercent!),
BatteryDisplayHelper.getBatteryIcon(
deviceInfo.batteryPercent!,
),
color: BatteryDisplayHelper.getBatteryColor(
deviceInfo.batteryPercent!,
),