mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-06 00:36:40 +10:00
Initial commit: MeshCore Open Flutter client
Open-source Flutter client for MeshCore LoRa mesh networking devices. Features: - BLE device scanning and connection - Nordic UART Service (NUS) integration - Material 3 design with system theme support - Provider-based state management - Placeholder screens for chat, contacts, and settings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
|
||||
class ChannelMessageStore {
|
||||
static const String _keyPrefix = 'channel_messages_';
|
||||
|
||||
/// Save messages for a specific channel
|
||||
Future<void> saveChannelMessages(int channelIndex, List<ChannelMessage> messages) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_keyPrefix$channelIndex';
|
||||
|
||||
// Convert messages to JSON
|
||||
final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
|
||||
final jsonString = jsonEncode(jsonList);
|
||||
|
||||
await prefs.setString(key, jsonString);
|
||||
}
|
||||
|
||||
/// Load messages for a specific channel
|
||||
Future<List<ChannelMessage>> loadChannelMessages(int channelIndex) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_keyPrefix$channelIndex';
|
||||
|
||||
final jsonString = prefs.getString(key);
|
||||
if (jsonString == null) return [];
|
||||
|
||||
try {
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
return jsonList.map((json) => _messageFromJson(json)).toList();
|
||||
} catch (e) {
|
||||
// If parsing fails, return empty list
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear messages for a specific channel
|
||||
Future<void> clearChannelMessages(int channelIndex) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_keyPrefix$channelIndex';
|
||||
await prefs.remove(key);
|
||||
}
|
||||
|
||||
/// Clear all channel messages
|
||||
Future<void> clearAllChannelMessages() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final keys = prefs.getKeys().where((k) => k.startsWith(_keyPrefix));
|
||||
for (var key in keys) {
|
||||
await prefs.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert ChannelMessage to JSON map
|
||||
Map<String, dynamic> _messageToJson(ChannelMessage msg) {
|
||||
return {
|
||||
'senderKey': msg.senderKey != null ? base64Encode(msg.senderKey!) : null,
|
||||
'senderName': msg.senderName,
|
||||
'text': msg.text,
|
||||
'timestamp': msg.timestamp.millisecondsSinceEpoch,
|
||||
'isOutgoing': msg.isOutgoing,
|
||||
'status': msg.status.index,
|
||||
'channelIndex': msg.channelIndex,
|
||||
'repeatCount': msg.repeatCount,
|
||||
'pathLength': msg.pathLength,
|
||||
'pathBytes': base64Encode(msg.pathBytes),
|
||||
'repeats': msg.repeats.map(_repeatToJson).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert JSON map to ChannelMessage
|
||||
ChannelMessage _messageFromJson(Map<String, dynamic> json) {
|
||||
final rawText = json['text'] as String;
|
||||
final decodedText = Smaz.tryDecodePrefixed(rawText) ?? rawText;
|
||||
return ChannelMessage(
|
||||
senderKey: json['senderKey'] != null
|
||||
? Uint8List.fromList(base64Decode(json['senderKey']))
|
||||
: null,
|
||||
senderName: json['senderName'] as String,
|
||||
text: decodedText,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
|
||||
isOutgoing: json['isOutgoing'] as bool,
|
||||
status: ChannelMessageStatus.values[json['status'] as int],
|
||||
repeatCount: (json['repeatCount'] as int?) ?? 0,
|
||||
pathLength: json['pathLength'] as int?,
|
||||
pathBytes: json['pathBytes'] != null
|
||||
? Uint8List.fromList(base64Decode(json['pathBytes'] as String))
|
||||
: Uint8List(0),
|
||||
repeats: (json['repeats'] as List<dynamic>?)
|
||||
?.map((entry) => _repeatFromJson(entry as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
channelIndex: json['channelIndex'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _repeatToJson(Repeat repeat) {
|
||||
return {
|
||||
'repeaterKey': repeat.repeaterKey != null ? base64Encode(repeat.repeaterKey!) : null,
|
||||
'repeaterName': repeat.repeaterName,
|
||||
'tripTimeMs': repeat.tripTimeMs,
|
||||
'path': repeat.path?.map((bytes) => base64Encode(bytes)).toList() ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
Repeat _repeatFromJson(Map<String, dynamic> json) {
|
||||
return Repeat(
|
||||
repeaterKey: json['repeaterKey'] != null
|
||||
? Uint8List.fromList(base64Decode(json['repeaterKey']))
|
||||
: null,
|
||||
repeaterName: json['repeaterName'] as String? ?? 'Unknown',
|
||||
tripTimeMs: json['tripTimeMs'] as int? ?? 0,
|
||||
path: (json['path'] as List<dynamic>?)
|
||||
?.map((entry) => Uint8List.fromList(base64Decode(entry as String)))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ChannelOrderStore {
|
||||
static const String _key = 'channel_order';
|
||||
|
||||
Future<void> saveChannelOrder(List<int> order) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_key, jsonEncode(order));
|
||||
}
|
||||
|
||||
Future<List<int>> loadChannelOrder() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_key);
|
||||
if (raw == null || raw.isEmpty) return [];
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is List) {
|
||||
return decoded.map((value) => value is int ? value : int.tryParse('$value')).whereType<int>().toList();
|
||||
}
|
||||
} catch (_) {
|
||||
// fall through to legacy parse
|
||||
}
|
||||
return raw
|
||||
.split(',')
|
||||
.map((value) => int.tryParse(value))
|
||||
.whereType<int>()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ChannelSettingsStore {
|
||||
static const String _smazKeyPrefix = 'channel_smaz_';
|
||||
|
||||
Future<bool> loadSmazEnabled(int channelIndex) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_smazKeyPrefix$channelIndex';
|
||||
return prefs.getBool(key) ?? false;
|
||||
}
|
||||
|
||||
Future<void> saveSmazEnabled(int channelIndex, bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_smazKeyPrefix$channelIndex';
|
||||
await prefs.setBool(key, enabled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact_group.dart';
|
||||
|
||||
class ContactGroupStore {
|
||||
static const String _key = 'contact_groups';
|
||||
|
||||
Future<List<ContactGroup>> loadGroups() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_key);
|
||||
if (raw == null || raw.isEmpty) return [];
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is List) {
|
||||
return decoded
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(ContactGroup.fromJson)
|
||||
.toList();
|
||||
}
|
||||
} catch (_) {
|
||||
// Return empty list on parse errors.
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> saveGroups(List<ContactGroup> groups) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final encoded = jsonEncode(groups.map((group) => group.toJson()).toList());
|
||||
await prefs.setString(_key, encoded);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/contact.dart';
|
||||
|
||||
class ContactStore {
|
||||
static const String _key = 'contacts';
|
||||
|
||||
Future<List<Contact>> loadContacts() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonStr = prefs.getString(_key);
|
||||
if (jsonStr == null) return [];
|
||||
|
||||
try {
|
||||
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
||||
return jsonList.map((entry) => _fromJson(entry as Map<String, dynamic>)).toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveContacts(List<Contact> contacts) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonList = contacts.map(_toJson).toList();
|
||||
await prefs.setString(_key, jsonEncode(jsonList));
|
||||
}
|
||||
|
||||
Map<String, dynamic> _toJson(Contact contact) {
|
||||
return {
|
||||
'publicKey': base64Encode(contact.publicKey),
|
||||
'name': contact.name,
|
||||
'type': contact.type,
|
||||
'pathLength': contact.pathLength,
|
||||
'path': base64Encode(contact.path),
|
||||
'latitude': contact.latitude,
|
||||
'longitude': contact.longitude,
|
||||
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
Contact _fromJson(Map<String, dynamic> json) {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
type: json['type'] as int? ?? 0,
|
||||
pathLength: json['pathLength'] as int? ?? -1,
|
||||
path: json['path'] != null
|
||||
? Uint8List.fromList(base64Decode(json['path'] as String))
|
||||
: Uint8List(0),
|
||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(json['lastSeen'] as int? ?? 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/message.dart';
|
||||
|
||||
class MessageStore {
|
||||
static const String _keyPrefix = 'messages_';
|
||||
|
||||
Future<void> saveMessages(String contactKeyHex, List<Message> messages) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_keyPrefix$contactKeyHex';
|
||||
final jsonList = messages.map(_messageToJson).toList();
|
||||
await prefs.setString(key, jsonEncode(jsonList));
|
||||
}
|
||||
|
||||
Future<List<Message>> loadMessages(String contactKeyHex) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_keyPrefix$contactKeyHex';
|
||||
final jsonString = prefs.getString(key);
|
||||
if (jsonString == null) return [];
|
||||
|
||||
try {
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
return jsonList.map((json) => _messageFromJson(json)).toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearMessages(String contactKeyHex) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_keyPrefix$contactKeyHex';
|
||||
await prefs.remove(key);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _messageToJson(Message msg) {
|
||||
return {
|
||||
'senderKey': base64Encode(msg.senderKey),
|
||||
'text': msg.text,
|
||||
'timestamp': msg.timestamp.millisecondsSinceEpoch,
|
||||
'isOutgoing': msg.isOutgoing,
|
||||
'isCli': msg.isCli,
|
||||
'status': msg.status.index,
|
||||
'messageId': msg.messageId,
|
||||
'retryCount': msg.retryCount,
|
||||
'estimatedTimeoutMs': msg.estimatedTimeoutMs,
|
||||
'expectedAckHash': msg.expectedAckHash != null ? base64Encode(msg.expectedAckHash!) : null,
|
||||
'sentAt': msg.sentAt?.millisecondsSinceEpoch,
|
||||
'deliveredAt': msg.deliveredAt?.millisecondsSinceEpoch,
|
||||
'tripTimeMs': msg.tripTimeMs,
|
||||
'forceFlood': msg.forceFlood,
|
||||
'pathLength': msg.pathLength,
|
||||
'pathBytes': msg.pathBytes.isNotEmpty ? base64Encode(msg.pathBytes) : null,
|
||||
};
|
||||
}
|
||||
|
||||
Message _messageFromJson(Map<String, dynamic> json) {
|
||||
return Message(
|
||||
senderKey: Uint8List.fromList(base64Decode(json['senderKey'] as String)),
|
||||
text: json['text'] as String,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
|
||||
isOutgoing: json['isOutgoing'] as bool,
|
||||
isCli: json['isCli'] as bool? ?? false,
|
||||
status: MessageStatus.values[json['status'] as int],
|
||||
messageId: json['messageId'] as String?,
|
||||
retryCount: json['retryCount'] as int? ?? 0,
|
||||
estimatedTimeoutMs: json['estimatedTimeoutMs'] as int?,
|
||||
expectedAckHash: json['expectedAckHash'] != null
|
||||
? Uint8List.fromList(base64Decode(json['expectedAckHash'] as String))
|
||||
: null,
|
||||
sentAt: json['sentAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(json['sentAt'] as int)
|
||||
: null,
|
||||
deliveredAt: json['deliveredAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(json['deliveredAt'] as int)
|
||||
: null,
|
||||
tripTimeMs: json['tripTimeMs'] as int?,
|
||||
forceFlood: json['forceFlood'] as bool? ?? false,
|
||||
pathLength: json['pathLength'] as int?,
|
||||
pathBytes: json['pathBytes'] != null
|
||||
? Uint8List.fromList(base64Decode(json['pathBytes'] as String))
|
||||
: Uint8List(0),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user