mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-17 06:10:47 +10:00
Merge branch 'dev-shareContacts' into dev-shareContact
This commit is contained in:
@@ -102,6 +102,14 @@ class BufferWriter {
|
|||||||
}
|
}
|
||||||
writeBytes(bytes);
|
writeBytes(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void writeHex(String hex) {
|
||||||
|
List<int> result = [];
|
||||||
|
for (int i = 0; i < hex.length ~/ 2; i++) {
|
||||||
|
result.add(int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16));
|
||||||
|
}
|
||||||
|
writeBytes(Uint8List.fromList(result));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command codes (to device)
|
// Command codes (to device)
|
||||||
@@ -164,6 +172,7 @@ const int respCodeContactMsgRecv = 7;
|
|||||||
const int respCodeChannelMsgRecv = 8;
|
const int respCodeChannelMsgRecv = 8;
|
||||||
const int respCodeCurrTime = 9;
|
const int respCodeCurrTime = 9;
|
||||||
const int respCodeNoMoreMessages = 10;
|
const int respCodeNoMoreMessages = 10;
|
||||||
|
const int respCodeExportContact = 11;
|
||||||
const int respCodeBattAndStorage = 12;
|
const int respCodeBattAndStorage = 12;
|
||||||
const int respCodeDeviceInfo = 13;
|
const int respCodeDeviceInfo = 13;
|
||||||
const int respCodeContactMsgRecvV3 = 16;
|
const int respCodeContactMsgRecvV3 = 16;
|
||||||
@@ -729,3 +738,21 @@ Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload})
|
|||||||
}
|
}
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build a export contact frame
|
||||||
|
// [cmd][pub_key x32 / if empty exports your contact info]
|
||||||
|
Uint8List buildExportContactFrame(Uint8List pubKey) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdExportContact);
|
||||||
|
writer.writeBytes(pubKey);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a import contact frame
|
||||||
|
// [cmd][contact_frame x98+]
|
||||||
|
Uint8List buildImportContactFrame(String contactFrame) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdImportContact);
|
||||||
|
writer.writeHex(contactFrame);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
+10
-1
@@ -1316,6 +1316,7 @@
|
|||||||
"pathTrace_failed": "Path trace failed.",
|
"pathTrace_failed": "Path trace failed.",
|
||||||
"pathTrace_notAvailable": "Path trace not available.",
|
"pathTrace_notAvailable": "Path trace not available.",
|
||||||
"pathTrace_refreshTooltip": "Refresh Path Trace.",
|
"pathTrace_refreshTooltip": "Refresh Path Trace.",
|
||||||
|
|
||||||
"contacts_pathTrace": "Path Trace",
|
"contacts_pathTrace": "Path Trace",
|
||||||
"contacts_ping": "Ping",
|
"contacts_ping": "Ping",
|
||||||
"contacts_repeaterPathTrace": "Path trace to repeater",
|
"contacts_repeaterPathTrace": "Path trace to repeater",
|
||||||
@@ -1328,6 +1329,14 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {"type": "String"}
|
"name": {"type": "String"}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
"contacts_clipboardEmpty": "Clipboard Is Empty.",
|
||||||
|
"contacts_invalidAdvertFormat": "Invalid Contact Data",
|
||||||
|
"contacts_contactImported": "Contact has been Imported.",
|
||||||
|
"contacts_contactImportFailed": "Contact Failed to Imported.",
|
||||||
|
"contacts_zeroHopAdvert":"Zero Hop Advert",
|
||||||
|
"contacts_floodAdvert":"Flood Advert",
|
||||||
|
"contacts_copyAdvertToClipboard":"Copy Advert to Clipboard",
|
||||||
|
"contacts_addContactFromClipboard":"Add Contact from Clipboard"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:meshcore_open/widgets/path_trace_dialog.dart';
|
import 'package:meshcore_open/widgets/path_trace_dialog.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
@@ -54,16 +54,21 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
List<ContactGroup> _groups = [];
|
List<ContactGroup> _groups = [];
|
||||||
Timer? _searchDebounce;
|
Timer? _searchDebounce;
|
||||||
|
|
||||||
|
bool _imported = false;
|
||||||
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadGroups();
|
_loadGroups();
|
||||||
|
_setupFrameListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchDebounce?.cancel();
|
_searchDebounce?.cancel();
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
|
_frameSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +84,74 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
await _groupStore.saveGroups(_groups);
|
await _groupStore.saveGroups(_groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setupFrameListener() {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
// Listen for incoming text messages from the repeater
|
||||||
|
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||||
|
if (frame.isEmpty) return;
|
||||||
|
final frameBuffer = BufferReader(frame);
|
||||||
|
final code = frameBuffer.readUInt8();
|
||||||
|
|
||||||
|
if (code == respCodeExportContact) {
|
||||||
|
final advertPacket = frameBuffer.readRemainingBytes();
|
||||||
|
final hexString = pubKeyToHex(advertPacket);
|
||||||
|
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(code == respCodeOk && _imported) {
|
||||||
|
// Show a snackbar indicating success
|
||||||
|
_imported = false;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_contactImported)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(code == respCodeErr && _imported) {
|
||||||
|
// Show a snackbar indicating success
|
||||||
|
_imported = false;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_contactImportFailed)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _contactExport(Uint8List pubKey) async {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
final exportContactFrame = buildExportContactFrame(pubKey);
|
||||||
|
await connector.sendFrame(exportContactFrame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _contactImport() async {
|
||||||
|
final clipboardData = await Clipboard.getData('text/plain');
|
||||||
|
if (clipboardData == null || clipboardData.text == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final text = clipboardData.text!.trim();
|
||||||
|
if (!text.startsWith('meshcore://')) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final hexString = text.substring('meshcore://'.length);
|
||||||
|
try {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
final importContactFrame = buildImportContactFrame(hexString);
|
||||||
|
await connector.sendFrame(importContactFrame);
|
||||||
|
_imported = true;
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final connector = context.watch<MeshCoreConnector>();
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
@@ -98,18 +171,87 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
PopupMenuButton(itemBuilder: (context) => [
|
||||||
icon: const Icon(Icons.bluetooth_disabled),
|
PopupMenuItem(
|
||||||
tooltip: context.l10n.common_disconnect,
|
child: Row(
|
||||||
onPressed: () => _disconnect(context, connector),
|
children: [
|
||||||
),
|
const Icon(Icons.connect_without_contact),
|
||||||
IconButton(
|
const SizedBox(width: 8),
|
||||||
icon: const Icon(Icons.tune),
|
Text(context.l10n.contacts_zeroHopAdvert),
|
||||||
tooltip: context.l10n.common_settings,
|
],
|
||||||
onPressed: () => Navigator.push(
|
),
|
||||||
context,
|
onTap: () => {
|
||||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
connector.sendSelfAdvert(flood: false),
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.cell_tower),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.contacts_floodAdvert),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => {
|
||||||
|
connector.sendSelfAdvert(flood: true),
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.copy),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.contacts_copyAdvertToClipboard),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _contactExport(Uint8List.fromList([])),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.paste),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.contacts_addContactFromClipboard),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _contactImport(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
icon: const Icon(Icons.connect_without_contact),
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.logout, color: Colors.red),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.common_disconnect),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _disconnect(context, connector),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.settings),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.settings_title),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -834,6 +976,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
_openChat(context, contact);
|
_openChat(context, contact);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete, color: Colors.red),
|
leading: const Icon(Icons.delete, color: Colors.red),
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -906,9 +1049,10 @@ class _ContactTile extends StatelessWidget {
|
|||||||
child: _buildContactAvatar(contact),
|
child: _buildContactAvatar(contact),
|
||||||
),
|
),
|
||||||
title: Text(contact.name),
|
title: Text(contact.name),
|
||||||
subtitle: Text(
|
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
'${contact.typeLabel} • ${contact.pathLabel} ${contact.shortPubKeyHex}',
|
Text(contact.pathLabel),
|
||||||
),
|
Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12))
|
||||||
|
],),
|
||||||
// Clamp text scaling in trailing section to prevent overflow while
|
// Clamp text scaling in trailing section to prevent overflow while
|
||||||
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
||||||
trailing: MediaQuery(
|
trailing: MediaQuery(
|
||||||
@@ -929,8 +1073,13 @@ class _ContactTile extends StatelessWidget {
|
|||||||
_formatLastSeen(context, lastSeen),
|
_formatLastSeen(context, lastSeen),
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
if (contact.hasLocation)
|
Row(
|
||||||
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (contact.hasLocation)
|
||||||
|
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user