mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
Merge branch 'dev' into telemetry-gps-map
This commit is contained in:
@@ -199,6 +199,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
double? _selfLongitude;
|
||||
final List<DirectRepeater> _directRepeaters = List.empty(growable: true);
|
||||
bool _isLoadingContacts = false;
|
||||
bool _hasLoadedContacts = false;
|
||||
bool _isLoadingChannels = false;
|
||||
bool _hasLoadedChannels = false;
|
||||
TimeoutPredictionService? _timeoutPredictionService;
|
||||
@@ -220,10 +221,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _batteryRequested = false;
|
||||
bool _awaitingSelfInfo = false;
|
||||
bool _hasReceivedDeviceInfo = false;
|
||||
// Initial sync is serialized for predictable progress. Firmware exposes one
|
||||
// FIFO queued-message stream, so direct/room frames are buffered until after
|
||||
// contacts are known.
|
||||
bool _pendingInitialChannelSync = false;
|
||||
bool _pendingInitialContactsSync = false;
|
||||
bool _pendingInitialQueuedMessageSync = false;
|
||||
bool _bleInitialSyncStarted = false;
|
||||
bool _pendingDeferredChannelSyncAfterContacts = false;
|
||||
bool _webInitialHandshakeRequestSent = false;
|
||||
bool _preserveContactsOnRefresh = false;
|
||||
bool _autoAddUsers = false;
|
||||
@@ -238,13 +242,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int _advertLocPolicy = 0;
|
||||
int _multiAcks = 0;
|
||||
|
||||
static const int _defaultMaxContacts = 32;
|
||||
static const int _defaultMaxChannels = 8;
|
||||
static const int _defaultMaxContacts = 350;
|
||||
static const int _defaultMaxChannels = 40;
|
||||
int _maxContacts = _defaultMaxContacts;
|
||||
int _maxChannels = _defaultMaxChannels;
|
||||
int? _contactSyncTotal;
|
||||
int _contactSyncReceived = 0;
|
||||
bool _contactSyncUsesSinceFilter = false;
|
||||
bool _isSyncingQueuedMessages = false;
|
||||
bool _deferQueuedContactMessagesUntilContacts = false;
|
||||
bool _isProcessingDeferredQueuedContactMessages = false;
|
||||
bool _queuedMessageSyncInFlight = false;
|
||||
bool _didInitialQueueSync = false;
|
||||
final List<Uint8List> _deferredQueuedContactMessageFrames = [];
|
||||
bool _pendingQueueSync = false;
|
||||
Timer? _queueSyncTimeout;
|
||||
int _queueSyncRetries = 0;
|
||||
@@ -373,7 +382,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
List<Channel> get channels => List.unmodifiable(_channels);
|
||||
bool get isConnected => _state == MeshCoreConnectionState.connected;
|
||||
bool get isLoadingContacts => _isLoadingContacts;
|
||||
bool get hasLoadedContacts => _hasLoadedContacts;
|
||||
bool get isLoadingChannels => _isLoadingChannels;
|
||||
bool get hasLoadedChannels => _hasLoadedChannels;
|
||||
Stream<Uint8List> get receivedFrames => _receivedFramesController.stream;
|
||||
Uint8List? get selfPublicKey => _selfPublicKey;
|
||||
String get selfPublicKeyHex => pubKeyToHex(_selfPublicKey ?? Uint8List(0));
|
||||
@@ -436,7 +447,16 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int get maxContacts => _maxContacts;
|
||||
int get maxChannels => _maxChannels;
|
||||
Set<String> get knownContactKeys => Set.unmodifiable(_knownContactKeys);
|
||||
bool get isSyncingQueuedMessages => _isSyncingQueuedMessages;
|
||||
double? get contactSyncProgress {
|
||||
final total = _contactSyncTotal;
|
||||
if (!_isLoadingContacts || total == null || total <= 0) return null;
|
||||
return (_contactSyncReceived / total).clamp(0.0, 1.0).toDouble();
|
||||
}
|
||||
|
||||
bool get isSyncingQueuedMessages =>
|
||||
_isSyncingQueuedMessages || _isProcessingDeferredQueuedContactMessages;
|
||||
bool get isShowingQueuedMessageSyncProgress =>
|
||||
_deferQueuedContactMessagesUntilContacts && isSyncingQueuedMessages;
|
||||
bool get isSyncingChannels => _isSyncingChannels;
|
||||
int get channelSyncProgress =>
|
||||
_isSyncingChannels && _totalChannelsToRequest > 0
|
||||
@@ -1106,19 +1126,31 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _translateIncomingContactMessage(
|
||||
Future<TranslationResult?> translateContactMessage(
|
||||
String contactKeyHex,
|
||||
Message message,
|
||||
) async {
|
||||
Message message, {
|
||||
bool manualTranslation = false,
|
||||
}) async {
|
||||
try {
|
||||
if (message.translatedText?.trim().isNotEmpty == true ||
|
||||
(!manualTranslation &&
|
||||
message.translationStatus != MessageTranslationStatus.none)) {
|
||||
return null;
|
||||
}
|
||||
final service = _translationService;
|
||||
if (service == null ||
|
||||
!service.shouldTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: message.isCli,
|
||||
isOutgoing: message.isOutgoing,
|
||||
)) {
|
||||
return;
|
||||
!(manualTranslation
|
||||
? service.canTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: message.isCli,
|
||||
isOutgoing: message.isOutgoing,
|
||||
)
|
||||
: service.shouldAutoTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: message.isCli,
|
||||
isOutgoing: message.isOutgoing,
|
||||
))) {
|
||||
return null;
|
||||
}
|
||||
final targetLanguageCode = service.resolvedIncomingLanguageCode(
|
||||
_appSettingsService?.settings.languageOverride,
|
||||
@@ -1128,7 +1160,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
);
|
||||
if (result == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
final translated = result.status == MessageTranslationStatus.completed
|
||||
? result.translatedText
|
||||
@@ -1143,24 +1175,38 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
translationModelId: result.modelId,
|
||||
),
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
appLogger.warn('Translation failed for contact message: $error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _translateIncomingChannelMessage(
|
||||
Future<TranslationResult?> translateChannelMessage(
|
||||
int channelIndex,
|
||||
ChannelMessage message,
|
||||
) async {
|
||||
ChannelMessage message, {
|
||||
bool manualTranslation = false,
|
||||
}) async {
|
||||
try {
|
||||
if (message.translatedText?.trim().isNotEmpty == true ||
|
||||
(!manualTranslation &&
|
||||
message.translationStatus != MessageTranslationStatus.none)) {
|
||||
return null;
|
||||
}
|
||||
final service = _translationService;
|
||||
if (service == null ||
|
||||
!service.shouldTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: false,
|
||||
isOutgoing: message.isOutgoing,
|
||||
)) {
|
||||
return;
|
||||
!(manualTranslation
|
||||
? service.canTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: false,
|
||||
isOutgoing: message.isOutgoing,
|
||||
)
|
||||
: service.shouldAutoTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: false,
|
||||
isOutgoing: message.isOutgoing,
|
||||
))) {
|
||||
return null;
|
||||
}
|
||||
final targetLanguageCode = service.resolvedIncomingLanguageCode(
|
||||
_appSettingsService?.settings.languageOverride,
|
||||
@@ -1170,11 +1216,16 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
);
|
||||
if (result == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
final translated = result.status == MessageTranslationStatus.completed
|
||||
var translated = result.status == MessageTranslationStatus.completed
|
||||
? result.translatedText
|
||||
: null;
|
||||
// Strip replyInfo prefix from translated text to match stored message.text
|
||||
if (translated != null) {
|
||||
final regex = RegExp(r'^@\[[^\]]+\]\s+', dotAll: true);
|
||||
translated = translated.replaceFirst(regex, '');
|
||||
}
|
||||
_updateStoredChannelMessage(
|
||||
channelIndex,
|
||||
message.messageId,
|
||||
@@ -1185,8 +1236,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
translationModelId: result.modelId,
|
||||
),
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
appLogger.warn('Translation failed for channel message: $error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1515,6 +1568,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_pendingInitialChannelSync = true;
|
||||
_pendingInitialQueuedMessageSync = true;
|
||||
_pendingInitialContactsSync = true;
|
||||
_appDebugLogService?.info(
|
||||
'connectUsb: requesting device info…',
|
||||
tag: 'USB',
|
||||
@@ -1625,6 +1680,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_pendingInitialChannelSync = true;
|
||||
_pendingInitialQueuedMessageSync = true;
|
||||
_pendingInitialContactsSync = true;
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
if (_radioStatsPollRefCount > 0) _startRadioStatsPolling();
|
||||
@@ -2259,7 +2316,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
_bleInitialSyncStarted = true;
|
||||
_pendingInitialChannelSync = true;
|
||||
_pendingInitialContactsSync = true;
|
||||
_pendingInitialQueuedMessageSync = true;
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
@@ -2274,7 +2333,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
await syncTime();
|
||||
unawaited(getChannels());
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
|
||||
void _resetConnectionHandshakeState() {
|
||||
@@ -2287,11 +2346,39 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfInfoRetryTimer?.cancel();
|
||||
_selfInfoRetryTimer = null;
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_resetSyncProgressState();
|
||||
_bleInitialSyncStarted = false;
|
||||
_pathHashByteWidth = 1;
|
||||
}
|
||||
|
||||
void _resetSyncProgressState() {
|
||||
_pendingInitialChannelSync = false;
|
||||
_pendingInitialContactsSync = false;
|
||||
_bleInitialSyncStarted = false;
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
_pathHashByteWidth = 1;
|
||||
_pendingInitialQueuedMessageSync = false;
|
||||
_contactSyncTotal = null;
|
||||
_contactSyncReceived = 0;
|
||||
_contactSyncUsesSinceFilter = false;
|
||||
_isLoadingContacts = false;
|
||||
_hasLoadedContacts = false;
|
||||
_isLoadingChannels = false;
|
||||
_hasLoadedChannels = false;
|
||||
_isSyncingQueuedMessages = false;
|
||||
_deferQueuedContactMessagesUntilContacts = false;
|
||||
_isProcessingDeferredQueuedContactMessages = false;
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_deferredQueuedContactMessageFrames.clear();
|
||||
_pendingQueueSync = false;
|
||||
_queueSyncTimeout?.cancel();
|
||||
_queueSyncTimeout = null;
|
||||
_queueSyncRetries = 0;
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_channelSyncTimeout?.cancel();
|
||||
_channelSyncTimeout = null;
|
||||
_channelSyncRetries = 0;
|
||||
_nextChannelIndexToRequest = 0;
|
||||
_totalChannelsToRequest = 0;
|
||||
_previousChannelsCache.clear();
|
||||
}
|
||||
|
||||
bool get _shouldAutoReconnect =>
|
||||
@@ -2428,17 +2515,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_batteryRequested = false;
|
||||
_awaitingSelfInfo = false;
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
_pendingInitialContactsSync = false;
|
||||
_maxContacts = _defaultMaxContacts;
|
||||
_maxChannels = _defaultMaxChannels;
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_didInitialQueueSync = false;
|
||||
_pendingQueueSync = false;
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_hasLoadedChannels = false;
|
||||
_resetSyncProgressState();
|
||||
_pendingChannelSentQueue.clear();
|
||||
_pendingGenericAckQueue.clear();
|
||||
_reactionSendQueueSequence = 0;
|
||||
@@ -2691,10 +2770,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
_isLoadingContacts = true;
|
||||
_preserveContactsOnRefresh = preserveExisting;
|
||||
_contactSyncTotal = null;
|
||||
_contactSyncReceived = 0;
|
||||
_contactSyncUsesSinceFilter = since != null;
|
||||
if (!preserveExisting) {
|
||||
_hasLoadedContacts = false;
|
||||
_contacts.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
notifyListeners();
|
||||
|
||||
await sendFrame(buildGetContactsFrame(since: since));
|
||||
}
|
||||
@@ -3292,11 +3375,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
Future<void> syncQueuedMessages({bool force = false}) async {
|
||||
if (!isConnected) return;
|
||||
if (!force && _isSyncingQueuedMessages) return;
|
||||
if (_isProcessingDeferredQueuedContactMessages) {
|
||||
_pendingQueueSync = true;
|
||||
return;
|
||||
}
|
||||
if (_awaitingSelfInfo || _isLoadingContacts) {
|
||||
_pendingQueueSync = true;
|
||||
return;
|
||||
}
|
||||
if (_isSyncingChannels || _channelSyncInFlight) {
|
||||
_pendingQueueSync = true;
|
||||
return;
|
||||
}
|
||||
_isSyncingQueuedMessages = true;
|
||||
notifyListeners();
|
||||
await _requestNextQueuedMessage();
|
||||
}
|
||||
|
||||
@@ -3330,6 +3422,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queueSyncTimeout?.cancel();
|
||||
_queueSyncRetries = 0;
|
||||
notifyListeners();
|
||||
_continueAfterQueuedMessageSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3349,6 +3443,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queueSyncRetries = 0;
|
||||
notifyListeners();
|
||||
_continueAfterQueuedMessageSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3448,6 +3544,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
_isLoadingChannels = true;
|
||||
_isSyncingChannels = true;
|
||||
_hasLoadedChannels = false;
|
||||
_previousChannelsCache = List<Channel>.from(_channels);
|
||||
_channels.clear();
|
||||
_nextChannelIndexToRequest = 0;
|
||||
@@ -3537,6 +3634,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_nextChannelIndexToRequest++;
|
||||
_channelSyncRetries = 0;
|
||||
_channelSyncInFlight = false;
|
||||
notifyListeners();
|
||||
unawaited(_requestNextChannel());
|
||||
}
|
||||
}
|
||||
@@ -3572,16 +3670,31 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (completed) {
|
||||
_hasLoadedChannels = true;
|
||||
_previousChannelsCache.clear();
|
||||
} else if (_channels.isEmpty && _previousChannelsCache.isNotEmpty) {
|
||||
// A failed initial sync should not leave the UI empty/spinning forever.
|
||||
// Restore the pre-sync list so cached channels remain usable.
|
||||
_channels.addAll(_previousChannelsCache);
|
||||
_applyChannelOrder();
|
||||
_recalculateCachedChannelsUnreadTotal();
|
||||
}
|
||||
|
||||
// Fallback: if contact sync was deferred waiting for channel 0 but
|
||||
// channel sync finished without triggering it, start contacts now.
|
||||
if (_pendingInitialContactsSync && isConnected) {
|
||||
_pendingInitialContactsSync = false;
|
||||
unawaited(getContacts());
|
||||
if (isConnected) {
|
||||
_startPostChannelInitialQueuedMessageSync();
|
||||
}
|
||||
|
||||
// Keep cache on failure/disconnection for future attempts
|
||||
if (!completed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _startPostChannelInitialQueuedMessageSync() {
|
||||
if (_pendingInitialQueuedMessageSync || _pendingQueueSync) {
|
||||
_deferQueuedContactMessagesUntilContacts = _pendingInitialContactsSync;
|
||||
_pendingInitialQueuedMessageSync = false;
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setChannel(int index, String name, Uint8List psk) async {
|
||||
@@ -3633,6 +3746,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_contacts.clear();
|
||||
}
|
||||
_isLoadingContacts = true;
|
||||
_contactSyncReceived = 0;
|
||||
// Firmware v3+ includes total contacts after CONTACTS_START.
|
||||
// Incremental sync reports total contacts, not filtered result count.
|
||||
if (frame.length >= 5 && !_contactSyncUsesSinceFilter) {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(1);
|
||||
_contactSyncTotal = reader.readUInt32LE();
|
||||
} else if (!_contactSyncUsesSinceFilter) {
|
||||
// Older firmwares may omit the count; use the nRF node capacity as
|
||||
// a conservative progress fallback instead of hiding the progress.
|
||||
_contactSyncTotal = _defaultMaxContacts;
|
||||
} else {
|
||||
_contactSyncTotal = null;
|
||||
}
|
||||
notifyListeners();
|
||||
break;
|
||||
case pushCodeAdvert:
|
||||
@@ -3650,7 +3777,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
case respCodeEndOfContacts:
|
||||
debugPrint('Got END_OF_CONTACTS');
|
||||
_isLoadingContacts = false;
|
||||
_hasLoadedContacts = true;
|
||||
_preserveContactsOnRefresh = false;
|
||||
_contactSyncUsesSinceFilter = false;
|
||||
unawaited(updateKnownDiscovered());
|
||||
notifyListeners();
|
||||
unawaited(_persistContacts());
|
||||
@@ -3660,23 +3789,21 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
!_channelSyncInFlight) {
|
||||
unawaited(_requestNextChannel());
|
||||
}
|
||||
if (!_didInitialQueueSync || _pendingQueueSync) {
|
||||
_didInitialQueueSync = true;
|
||||
if (_deferQueuedContactMessagesUntilContacts) {
|
||||
unawaited(_processDeferredQueuedContactMessages());
|
||||
} else if (_pendingQueueSync) {
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
if (_pendingDeferredChannelSyncAfterContacts &&
|
||||
(_activeTransport == MeshCoreTransportType.bluetooth ||
|
||||
_activeTransport == MeshCoreTransportType.usb ||
|
||||
_activeTransport == MeshCoreTransportType.tcp)) {
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
unawaited(getChannels());
|
||||
}
|
||||
break;
|
||||
case respCodeContactMsgRecv:
|
||||
case respCodeContactMsgRecvV3:
|
||||
_handleIncomingMessage(frame);
|
||||
if (_shouldDeferQueuedContactMessage(frame)) {
|
||||
_deferredQueuedContactMessageFrames.add(Uint8List.fromList(frame));
|
||||
_handleQueuedMessageReceived();
|
||||
} else {
|
||||
unawaited(_handleIncomingMessage(frame));
|
||||
}
|
||||
break;
|
||||
case respCodeChannelMsgRecv:
|
||||
case respCodeChannelMsgRecvV3:
|
||||
@@ -3860,23 +3987,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfInfoRetryTimer = null;
|
||||
notifyListeners();
|
||||
|
||||
// Auto-fetch contacts after getting self info. On web BLE, defer this
|
||||
// until after channel 0 so startup writes stay serialized.
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth) {
|
||||
_pendingInitialContactsSync = true;
|
||||
} else if (_activeTransport == MeshCoreTransportType.usb ||
|
||||
_activeTransport == MeshCoreTransportType.tcp) {
|
||||
_pendingDeferredChannelSyncAfterContacts = true;
|
||||
getContacts();
|
||||
} else {
|
||||
getContacts();
|
||||
}
|
||||
if (_shouldGateInitialChannelSync &&
|
||||
_activeTransport != MeshCoreTransportType.usb &&
|
||||
_activeTransport != MeshCoreTransportType.tcp) {
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
// Start the serialized initial sync pipeline after SELF_INFO.
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
|
||||
void _handleDeviceInfo(Uint8List frame) {
|
||||
@@ -3916,7 +4028,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
|
||||
if (isConnected &&
|
||||
_selfPublicKey != null &&
|
||||
(!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) {
|
||||
!_pendingInitialChannelSync) {
|
||||
unawaited(getChannels(maxChannels: nextMaxChannels));
|
||||
}
|
||||
}
|
||||
@@ -3931,12 +4043,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (!_pendingInitialChannelSync || !isConnected) {
|
||||
return;
|
||||
}
|
||||
if (_selfPublicKey == null || !_hasReceivedDeviceInfo) {
|
||||
if (_selfPublicKey == null ||
|
||||
(_shouldGateInitialChannelSync && !_hasReceivedDeviceInfo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingInitialChannelSync = false;
|
||||
unawaited(getChannels(maxChannels: _maxChannels));
|
||||
unawaited(getChannels(maxChannels: _maxChannels, force: true));
|
||||
}
|
||||
|
||||
void _handleNoMoreMessages() {
|
||||
@@ -3945,6 +4058,64 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_queueSyncRetries = 0; // Reset retry counter on successful completion
|
||||
notifyListeners();
|
||||
_continueAfterQueuedMessageSync();
|
||||
}
|
||||
|
||||
bool _shouldDeferQueuedContactMessage(Uint8List frame) {
|
||||
if (!_deferQueuedContactMessagesUntilContacts ||
|
||||
!_isSyncingQueuedMessages) {
|
||||
return false;
|
||||
}
|
||||
if (frame.isEmpty) return false;
|
||||
return frame[0] == respCodeContactMsgRecv ||
|
||||
frame[0] == respCodeContactMsgRecvV3;
|
||||
}
|
||||
|
||||
void _continueAfterQueuedMessageSync() {
|
||||
if (!_deferQueuedContactMessagesUntilContacts) return;
|
||||
if (_pendingInitialContactsSync && isConnected) {
|
||||
_pendingInitialContactsSync = false;
|
||||
unawaited(getContacts());
|
||||
return;
|
||||
}
|
||||
unawaited(_processDeferredQueuedContactMessages());
|
||||
}
|
||||
|
||||
Future<void> _processDeferredQueuedContactMessages() async {
|
||||
if (!_deferQueuedContactMessagesUntilContacts ||
|
||||
_isProcessingDeferredQueuedContactMessages) {
|
||||
return;
|
||||
}
|
||||
if (_deferredQueuedContactMessageFrames.isEmpty) {
|
||||
_deferQueuedContactMessagesUntilContacts = false;
|
||||
notifyListeners();
|
||||
if (_pendingQueueSync && isConnected) {
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_isProcessingDeferredQueuedContactMessages = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
// Replay direct/room queued messages only after contacts are loaded, so
|
||||
// sender prefixes can be resolved against the current contact list.
|
||||
while (_deferredQueuedContactMessageFrames.isNotEmpty) {
|
||||
final frame = _deferredQueuedContactMessageFrames.removeAt(0);
|
||||
await _handleIncomingMessage(frame);
|
||||
}
|
||||
} finally {
|
||||
_deferQueuedContactMessagesUntilContacts = false;
|
||||
_isProcessingDeferredQueuedContactMessages = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
if (_pendingQueueSync && isConnected) {
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
}
|
||||
|
||||
void _handleQueuedMessageReceived() {
|
||||
@@ -3953,6 +4124,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_queueSyncTimeout?.cancel(); // Cancel timeout - message arrived
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_queueSyncRetries = 0; // Reset retry counter on successful message
|
||||
notifyListeners();
|
||||
unawaited(_requestNextQueuedMessage());
|
||||
}
|
||||
|
||||
@@ -4094,11 +4266,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
void _handleContact(Uint8List frame, {bool isContact = true}) {
|
||||
final contactTmp = Contact.fromFrame(frame);
|
||||
if (contactTmp != null) {
|
||||
if (isContact && _isLoadingContacts) {
|
||||
_contactSyncReceived++;
|
||||
}
|
||||
if (listEquals(contactTmp.publicKey, _selfPublicKey)) {
|
||||
appLogger.info(
|
||||
'Ignoring contact with self public key: ${contactTmp.name}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
notifyListeners();
|
||||
removeContact(contactTmp);
|
||||
return;
|
||||
}
|
||||
@@ -4162,6 +4338,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
"Discovered contact ${contact.name} (type ${contact.typeLabelRaw}) not added due to auto-add settings",
|
||||
tag: 'Connector',
|
||||
);
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4370,9 +4547,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
void _handleIncomingMessage(Uint8List frame) async {
|
||||
Future<void> _handleIncomingMessage(Uint8List frame) async {
|
||||
if (_selfPublicKey == null) return;
|
||||
|
||||
// If we're syncing the queued messages, advance the queue immediately
|
||||
// before any potentially long async work (like translation/notifications).
|
||||
if (_isSyncingQueuedMessages) {
|
||||
_handleQueuedMessageReceived();
|
||||
}
|
||||
|
||||
var message = _parseContactMessage(frame);
|
||||
|
||||
// If message parsing failed due to unknown contact, refresh contacts and retry
|
||||
@@ -4438,35 +4621,52 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
_addMessage(message.senderKeyHex, message);
|
||||
if (!message.isOutgoing) {
|
||||
unawaited(
|
||||
_translateIncomingContactMessage(message.senderKeyHex, message),
|
||||
);
|
||||
}
|
||||
_maybeIncrementContactUnread(message);
|
||||
notifyListeners();
|
||||
|
||||
// Show notification for new incoming message
|
||||
// Show notification for new incoming message (run async with translation)
|
||||
if (!message.isOutgoing &&
|
||||
!message.isCli &&
|
||||
_appSettingsService != null) {
|
||||
final settings = _appSettingsService!.settings;
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewMessage) {
|
||||
if (contact?.type == advTypeChat) {
|
||||
_notificationService.showMessageNotification(
|
||||
contactName: contact?.name ?? 'Unknown',
|
||||
message: message.text,
|
||||
contactId: message.senderKeyHex,
|
||||
badgeCount: getTotalUnreadCount(),
|
||||
final msg = message; // capture for closure
|
||||
final c = contact; // capture contact reference
|
||||
unawaited(() async {
|
||||
final translationResult = await translateContactMessage(
|
||||
msg.senderKeyHex,
|
||||
msg,
|
||||
);
|
||||
} else if (contact?.type == advTypeRoom) {
|
||||
_notificationService.showMessageNotification(
|
||||
contactName: contact?.name ?? 'Unknown Room',
|
||||
message: message.text,
|
||||
contactId: message.senderKeyHex,
|
||||
badgeCount: getTotalUnreadCount(),
|
||||
);
|
||||
}
|
||||
if (c?.type == advTypeChat) {
|
||||
final resolvedText =
|
||||
(translationResult != null &&
|
||||
translationResult.status ==
|
||||
MessageTranslationStatus.completed &&
|
||||
translationResult.translatedText.trim().isNotEmpty)
|
||||
? translationResult.translatedText.trim()
|
||||
: msg.text.trim();
|
||||
await _notificationService.showMessageNotification(
|
||||
contactName: c?.name ?? 'Unknown',
|
||||
message: resolvedText,
|
||||
contactId: msg.senderKeyHex,
|
||||
badgeCount: getTotalUnreadCount(),
|
||||
);
|
||||
} else if (c?.type == advTypeRoom) {
|
||||
final resolvedText =
|
||||
(translationResult != null &&
|
||||
translationResult.status ==
|
||||
MessageTranslationStatus.completed &&
|
||||
translationResult.translatedText.trim().isNotEmpty)
|
||||
? translationResult.translatedText.trim()
|
||||
: msg.text.trim();
|
||||
await _notificationService.showMessageNotification(
|
||||
contactName: c?.name ?? 'Unknown Room',
|
||||
message: resolvedText,
|
||||
contactId: msg.senderKeyHex,
|
||||
badgeCount: getTotalUnreadCount(),
|
||||
);
|
||||
}
|
||||
}());
|
||||
}
|
||||
}
|
||||
_handleQueuedMessageReceived();
|
||||
@@ -4740,6 +4940,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
void _maybeNotifyChannelMessage(
|
||||
ChannelMessage message, {
|
||||
String? channelName,
|
||||
TranslationResult? translationResult,
|
||||
}) {
|
||||
if (message.isOutgoing || _appSettingsService == null) return;
|
||||
final channelIndex = message.channelIndex;
|
||||
@@ -4753,16 +4954,30 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final label = channelName ?? _channelDisplayName(channelIndex);
|
||||
if (_appSettingsService!.isChannelMuted(label)) return;
|
||||
|
||||
_notificationService.showChannelMessageNotification(
|
||||
channelName: label,
|
||||
senderName: message.senderName,
|
||||
message: message.text,
|
||||
channelIndex: channelIndex,
|
||||
badgeCount: getTotalUnreadCount(),
|
||||
);
|
||||
// Reuse translation result only if completed and non-empty; else use original text
|
||||
final resolvedText =
|
||||
(translationResult != null &&
|
||||
translationResult.status == MessageTranslationStatus.completed &&
|
||||
translationResult.translatedText.trim().isNotEmpty)
|
||||
? translationResult.translatedText.trim()
|
||||
: message.text.trim();
|
||||
unawaited(() async {
|
||||
await _notificationService.showChannelMessageNotification(
|
||||
channelName: label,
|
||||
senderName: message.senderName,
|
||||
message: resolvedText,
|
||||
channelIndex: message.channelIndex,
|
||||
badgeCount: getTotalUnreadCount(),
|
||||
);
|
||||
}());
|
||||
}
|
||||
|
||||
void _handleIncomingChannelMessage(Uint8List frame) {
|
||||
void _handleIncomingChannelMessage(Uint8List frame) async {
|
||||
// If we're syncing the queued messages, advance the queue immediately
|
||||
// before any potentially long async work (like translation/notifications).
|
||||
if (_isSyncingQueuedMessages) {
|
||||
_handleQueuedMessageReceived();
|
||||
}
|
||||
final parsed = ChannelMessage.fromFrame(frame);
|
||||
if (parsed != null && parsed.channelIndex != null) {
|
||||
if (_shouldDropSelfChannelMessage(parsed.senderName, parsed.pathBytes)) {
|
||||
@@ -4781,15 +4996,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
pathBytes: message.pathBytes,
|
||||
);
|
||||
final isNew = _addChannelMessage(message.channelIndex!, message);
|
||||
if (isNew && !message.isOutgoing) {
|
||||
unawaited(
|
||||
_translateIncomingChannelMessage(message.channelIndex!, message),
|
||||
);
|
||||
}
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
notifyListeners();
|
||||
if (isNew) {
|
||||
_maybeNotifyChannelMessage(message);
|
||||
if (isNew && !message.isOutgoing) {
|
||||
final msg = message; // capture for closure
|
||||
unawaited(() async {
|
||||
final translationResult = await translateChannelMessage(
|
||||
msg.channelIndex!,
|
||||
msg,
|
||||
);
|
||||
_maybeNotifyChannelMessage(msg, translationResult: translationResult);
|
||||
}());
|
||||
}
|
||||
_handleQueuedMessageReceived();
|
||||
} else if (_isSyncingQueuedMessages) {
|
||||
@@ -4797,7 +5014,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLogRxData(Uint8List frame) {
|
||||
void _handleLogRxData(Uint8List frame) async {
|
||||
if (frame.length < 4) return;
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
@@ -4865,16 +5082,24 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
pathBytes: message.pathBytes,
|
||||
);
|
||||
final isNew = _addChannelMessage(channel.index, message);
|
||||
if (isNew && !message.isOutgoing) {
|
||||
unawaited(_translateIncomingChannelMessage(channel.index, message));
|
||||
}
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
notifyListeners();
|
||||
if (isNew) {
|
||||
final label = channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name;
|
||||
_maybeNotifyChannelMessage(message, channelName: label);
|
||||
// Run translation + notification asynchronously to avoid blocking
|
||||
unawaited(() async {
|
||||
final translationResult = await translateChannelMessage(
|
||||
channel.index,
|
||||
message,
|
||||
);
|
||||
final label = channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name;
|
||||
_maybeNotifyChannelMessage(
|
||||
message,
|
||||
channelName: label,
|
||||
translationResult: translationResult,
|
||||
);
|
||||
}());
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
@@ -5092,14 +5317,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
// Move to next channel
|
||||
_nextChannelIndexToRequest++;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
channel.index == 0 &&
|
||||
_pendingInitialContactsSync) {
|
||||
_pendingInitialContactsSync = false;
|
||||
unawaited(getContacts());
|
||||
return;
|
||||
}
|
||||
notifyListeners();
|
||||
unawaited(_requestNextChannel());
|
||||
return;
|
||||
} else {
|
||||
@@ -5799,14 +6017,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Preserve deviceId and displayName for UI display during reconnection
|
||||
// They're only cleared on manual disconnect via disconnect() method
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
_pendingInitialContactsSync = false;
|
||||
_maxContacts = _defaultMaxContacts;
|
||||
_maxChannels = _defaultMaxChannels;
|
||||
_isSyncingQueuedMessages = false;
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_resetSyncProgressState();
|
||||
_pendingChannelSentQueue.clear();
|
||||
_pendingGenericAckQueue.clear();
|
||||
_reactionSendQueueSequence = 0;
|
||||
|
||||
@@ -2138,6 +2138,9 @@
|
||||
"translation_composerTitle": "Преведете преди да изпратите",
|
||||
"translation_enableSubtitle": "Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.",
|
||||
"translation_composerSubtitle": "Контролира началния статус на иконата за превод, създадена от композитора.",
|
||||
"translation_autoIncomingTitle": "Автоматичен превод на съобщения",
|
||||
"translation_autoIncomingSubtitle": "Превежда автоматично съобщенията за известия, както и за чатове или канали.",
|
||||
"translation_translateMessage": "Преведи съобщението",
|
||||
"translation_targetLanguage": "Целеви език",
|
||||
"translation_useAppLanguage": "Използвайте езика на приложението",
|
||||
"translation_downloadedModelLabel": "Изтегнат модел",
|
||||
|
||||
@@ -2166,6 +2166,9 @@
|
||||
"translation_enableSubtitle": "Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.",
|
||||
"translation_enableTitle": "Aktivieren Sie die Übersetzung",
|
||||
"translation_composerSubtitle": "Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.",
|
||||
"translation_autoIncomingTitle": "Nachrichten automatisch übersetzen",
|
||||
"translation_autoIncomingSubtitle": "Übersetzt Nachrichten für Benachrichtigungen sowie für Chats oder Kanäle automatisch.",
|
||||
"translation_translateMessage": "Nachricht übersetzen",
|
||||
"translation_targetLanguage": "Zielsprache",
|
||||
"translation_useAppLanguage": "Verwenden Sie die App-Sprache",
|
||||
"translation_downloadedModelLabel": "Heruntergeladenes Modell",
|
||||
|
||||
@@ -2404,6 +2404,9 @@
|
||||
"translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.",
|
||||
"translation_composerTitle": "Translate before sending",
|
||||
"translation_composerSubtitle": "Controls the default state of the composer translation icon.",
|
||||
"translation_autoIncomingTitle": "Auto-translate incoming messages",
|
||||
"translation_autoIncomingSubtitle": "Translates Messages for notification and for chat or channel automatically.",
|
||||
"translation_translateMessage": "Translate message",
|
||||
"translation_targetLanguage": "Target language",
|
||||
"translation_useAppLanguage": "Use app language",
|
||||
"translation_downloadedModelLabel": "Downloaded model",
|
||||
|
||||
@@ -2167,6 +2167,9 @@
|
||||
"translation_enableTitle": "Habilitar la traducción",
|
||||
"translation_composerTitle": "Traducir antes de enviar",
|
||||
"translation_composerSubtitle": "Controla el estado predeterminado del icono de traducción del compositor.",
|
||||
"translation_autoIncomingTitle": "Traducir mensajes automáticamente",
|
||||
"translation_autoIncomingSubtitle": "Traduce mensajes para notificaciones y para chats o canales automáticamente.",
|
||||
"translation_translateMessage": "Traducir mensaje",
|
||||
"translation_targetLanguage": "Idioma de destino",
|
||||
"translation_useAppLanguage": "Utilizar el idioma de la aplicación",
|
||||
"translation_downloadedModelLabel": "Modelo descargado",
|
||||
|
||||
@@ -2138,6 +2138,9 @@
|
||||
"translation_title": "Traduction",
|
||||
"translation_enableSubtitle": "Traduire les messages entrants et permettre la traduction avant l'envoi.",
|
||||
"translation_composerSubtitle": "Contrôle l'état par défaut de l'icône de traduction du composant.",
|
||||
"translation_autoIncomingTitle": "Traduire automatiquement les messages",
|
||||
"translation_autoIncomingSubtitle": "Traduit automatiquement les messages pour les notifications et pour les discussions ou les canaux.",
|
||||
"translation_translateMessage": "Traduire le message",
|
||||
"translation_targetLanguage": "Langue cible",
|
||||
"translation_useAppLanguage": "Utiliser la langue de l'application",
|
||||
"translation_downloadedModelLabel": "Modèle téléchargé",
|
||||
|
||||
@@ -2176,6 +2176,9 @@
|
||||
"translation_enableSubtitle": "Fordítsa az érkező üzeneteket, és lehetővé tegye a küldés előtti fordítást.",
|
||||
"translation_composerTitle": "Fordítsa el, mielőtt elküldi",
|
||||
"translation_composerSubtitle": "Ellenőrzi a zeneszerző fordítási ikon alapértékét.",
|
||||
"translation_autoIncomingTitle": "Üzenetek automatikus fordítása",
|
||||
"translation_autoIncomingSubtitle": "Automatikusan lefordítja az üzeneteket az értesítésekhez, valamint a csevegésekhez vagy csatornákhoz.",
|
||||
"translation_translateMessage": "Üzenet fordítása",
|
||||
"translation_targetLanguage": "Célnyelv",
|
||||
"translation_useAppLanguage": "Használja az alkalmazás nyelvének beállítását.",
|
||||
"translation_downloadedModelLabel": "Letöltött modell",
|
||||
|
||||
@@ -2139,6 +2139,9 @@
|
||||
"translation_enableTitle": "Abilitare la traduzione",
|
||||
"translation_title": "Traduzione",
|
||||
"translation_composerSubtitle": "Controlla lo stato predefinito dell'icona di traduzione del compositore.",
|
||||
"translation_autoIncomingTitle": "Traduci automaticamente i messaggi",
|
||||
"translation_autoIncomingSubtitle": "Traduce automaticamente i messaggi per le notifiche e per le chat o i canali.",
|
||||
"translation_translateMessage": "Traduci messaggio",
|
||||
"translation_targetLanguage": "Lingua di destinazione",
|
||||
"translation_useAppLanguage": "Utilizza la lingua dell'app",
|
||||
"translation_downloadedModelLabel": "Modello scaricato",
|
||||
|
||||
@@ -2176,6 +2176,9 @@
|
||||
"translation_composerTitle": "送信する前に翻訳する",
|
||||
"translation_enableTitle": "翻訳機能を有効にする",
|
||||
"translation_composerSubtitle": "作曲家翻訳アイコンのデフォルト状態を制御する。",
|
||||
"translation_autoIncomingTitle": "メッセージを自動翻訳",
|
||||
"translation_autoIncomingSubtitle": "通知やチャット、チャンネルのメッセージを自動的に翻訳します。",
|
||||
"translation_translateMessage": "メッセージを翻訳",
|
||||
"translation_targetLanguage": "翻訳対象言語",
|
||||
"translation_useAppLanguage": "アプリの言語設定",
|
||||
"translation_downloadedModelLabel": "ダウンロードしたモデル",
|
||||
|
||||
@@ -2176,6 +2176,9 @@
|
||||
"translation_enableTitle": "번역 기능 활성화",
|
||||
"translation_composerTitle": "보내기 전에 번역",
|
||||
"translation_composerSubtitle": "컴포저 번역 아이콘의 기본 상태를 제어합니다.",
|
||||
"translation_autoIncomingTitle": "메시지 자동 번역",
|
||||
"translation_autoIncomingSubtitle": "알림과 채팅 또는 채널의 메시지를 자동으로 번역합니다.",
|
||||
"translation_translateMessage": "메시지 번역",
|
||||
"translation_targetLanguage": "목표 언어",
|
||||
"translation_useAppLanguage": "앱 언어 사용",
|
||||
"translation_downloadedModelLabel": "다운로드한 모델",
|
||||
|
||||
@@ -7348,6 +7348,24 @@ abstract class AppLocalizations {
|
||||
/// **'Controls the default state of the composer translation icon.'**
|
||||
String get translation_composerSubtitle;
|
||||
|
||||
/// No description provided for @translation_autoIncomingTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto-translate incoming messages'**
|
||||
String get translation_autoIncomingTitle;
|
||||
|
||||
/// No description provided for @translation_autoIncomingSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translates Messages for notification and for chat or channel automatically.'**
|
||||
String get translation_autoIncomingSubtitle;
|
||||
|
||||
/// No description provided for @translation_translateMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translate message'**
|
||||
String get translation_translateMessage;
|
||||
|
||||
/// No description provided for @translation_targetLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -4300,6 +4300,16 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Контролира началния статус на иконата за превод, създадена от композитора.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle => 'Автоматичен превод на съобщения';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Превежда автоматично съобщенията за известия, както и за чатове или канали.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Преведи съобщението';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Целеви език';
|
||||
|
||||
|
||||
@@ -4315,6 +4315,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Nachrichten automatisch übersetzen';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Übersetzt Nachrichten für Benachrichtigungen sowie für Chats oder Kanäle automatisch.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Nachricht übersetzen';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Zielsprache';
|
||||
|
||||
|
||||
@@ -4224,6 +4224,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Controls the default state of the composer translation icon.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Auto-translate incoming messages';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Translates Messages for notification and for chat or channel automatically.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Translate message';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Target language';
|
||||
|
||||
|
||||
@@ -4302,6 +4302,17 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Controla el estado predeterminado del icono de traducción del compositor.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Traducir mensajes automáticamente';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Traduce mensajes para notificaciones y para chats o canales automáticamente.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Traducir mensaje';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Idioma de destino';
|
||||
|
||||
|
||||
@@ -4332,6 +4332,17 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Contrôle l\'état par défaut de l\'icône de traduction du composant.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Traduire automatiquement les messages';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Traduit automatiquement les messages pour les notifications et pour les discussions ou les canaux.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Traduire le message';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Langue cible';
|
||||
|
||||
|
||||
@@ -4320,6 +4320,16 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Ellenőrzi a zeneszerző fordítási ikon alapértékét.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle => 'Üzenetek automatikus fordítása';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Automatikusan lefordítja az üzeneteket az értesítésekhez, valamint a csevegésekhez vagy csatornákhoz.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Üzenet fordítása';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Célnyelv';
|
||||
|
||||
|
||||
@@ -4308,6 +4308,17 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Controlla lo stato predefinito dell\'icona di traduzione del compositore.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Traduci automaticamente i messaggi';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Traduce automaticamente i messaggi per le notifiche e per le chat o i canali.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Traduci messaggio';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Lingua di destinazione';
|
||||
|
||||
|
||||
@@ -4080,6 +4080,16 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get translation_composerSubtitle => '作曲家翻訳アイコンのデフォルト状態を制御する。';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle => 'メッセージを自動翻訳';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'通知やチャット、チャンネルのメッセージを自動的に翻訳します。';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'メッセージを翻訳';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => '翻訳対象言語';
|
||||
|
||||
|
||||
@@ -4081,6 +4081,16 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get translation_composerSubtitle => '컴포저 번역 아이콘의 기본 상태를 제어합니다.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle => '메시지 자동 번역';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'알림과 채팅 또는 채널의 메시지를 자동으로 번역합니다.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => '메시지 번역';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => '목표 언어';
|
||||
|
||||
|
||||
@@ -4285,6 +4285,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle => 'Berichten automatisch vertalen';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Vertaalt berichten automatisch voor meldingen en voor chats of kanalen.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Bericht vertalen';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Doeltaal';
|
||||
|
||||
|
||||
@@ -4323,6 +4323,17 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Kontroluje domyślny stan ikony tłumaczenia w edytorze.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Automatycznie tłumacz wiadomości';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Automatycznie tłumaczy wiadomości do powiadomień oraz do czatów lub kanałów.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Przetłumacz wiadomość';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Język docelowy';
|
||||
|
||||
|
||||
@@ -4298,6 +4298,17 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Controla o estado padrão do ícone de tradução do compositor.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Traduzir mensagens automaticamente';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Traduz automaticamente mensagens para notificações e para chats ou canais.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Traduzir mensagem';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Língua-alvo';
|
||||
|
||||
|
||||
@@ -4316,6 +4316,17 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Управляет исходным состоянием значка перевода, предоставляемого редактором.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Автоматически переводить сообщения';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Автоматически переводит сообщения для уведомлений, а также для чатов и каналов.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Перевести сообщение';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Целевой язык';
|
||||
|
||||
|
||||
@@ -4280,6 +4280,16 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Riadi výchoce stav ikony pre preklad, ktorú používa program.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle => 'Automaticky prekladať správy';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Automaticky prekladá správy pre upozornenia aj pre čet alebo kanál.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Preložiť správu';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Cieľový jazyk';
|
||||
|
||||
|
||||
@@ -4278,6 +4278,16 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle => 'Samodejno prevajaj sporočila';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Samodejno prevaja sporočila za obvestila ter za klepete ali kanale.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Prevedi sporočilo';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Ciljna jezika';
|
||||
|
||||
|
||||
@@ -4252,6 +4252,17 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Styr standardtillståndet för kompositorns översättningsikon.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Översätt meddelanden automatiskt';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Översätter meddelanden automatiskt för aviseringar och för chattar eller kanaler.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Översätt meddelande';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Målmedvetet språk';
|
||||
|
||||
|
||||
@@ -4315,6 +4315,17 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get translation_composerSubtitle =>
|
||||
'Контролює стан ікон перекладу, який використовується за замовчуванням.';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle =>
|
||||
'Автоматично перекладати повідомлення';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle =>
|
||||
'Автоматично перекладає повідомлення для сповіщень, а також для чатів і каналів.';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => 'Перекласти повідомлення';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Цільова мова';
|
||||
|
||||
|
||||
@@ -3955,6 +3955,15 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get translation_composerSubtitle => '控制作曲家翻译图标的默认状态。';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingTitle => '自动翻译消息';
|
||||
|
||||
@override
|
||||
String get translation_autoIncomingSubtitle => '自动为通知以及聊天或频道翻译消息。';
|
||||
|
||||
@override
|
||||
String get translation_translateMessage => '翻译消息';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => '目标语言';
|
||||
|
||||
|
||||
@@ -2140,6 +2140,9 @@
|
||||
"translation_composerTitle": "Vertaal voor verzending",
|
||||
"translation_composerSubtitle": "Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.",
|
||||
"translation_useAppLanguage": "Gebruik de taal van de app",
|
||||
"translation_autoIncomingTitle": "Berichten automatisch vertalen",
|
||||
"translation_autoIncomingSubtitle": "Vertaalt berichten automatisch voor meldingen en voor chats of kanalen.",
|
||||
"translation_translateMessage": "Bericht vertalen",
|
||||
"translation_targetLanguage": "Doeltaal",
|
||||
"translation_downloadedModelLabel": "Gedownloade model",
|
||||
"translation_presetModelLabel": "Voorgeprogrammeerd Hugging Face-model",
|
||||
|
||||
@@ -2176,6 +2176,9 @@
|
||||
"translation_enableTitle": "Włącz tłumaczenie",
|
||||
"translation_enableSubtitle": "Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.",
|
||||
"translation_composerSubtitle": "Kontroluje domyślny stan ikony tłumaczenia w edytorze.",
|
||||
"translation_autoIncomingTitle": "Automatycznie tłumacz wiadomości",
|
||||
"translation_autoIncomingSubtitle": "Automatycznie tłumaczy wiadomości do powiadomień oraz do czatów lub kanałów.",
|
||||
"translation_translateMessage": "Przetłumacz wiadomość",
|
||||
"translation_targetLanguage": "Język docelowy",
|
||||
"translation_useAppLanguage": "Użyj języka aplikacji",
|
||||
"translation_downloadedModelLabel": "Pobudowany model",
|
||||
|
||||
@@ -2139,6 +2139,9 @@
|
||||
"translation_enableTitle": "Ativar a tradução",
|
||||
"translation_title": "Tradução",
|
||||
"translation_composerSubtitle": "Controla o estado padrão do ícone de tradução do compositor.",
|
||||
"translation_autoIncomingTitle": "Traduzir mensagens automaticamente",
|
||||
"translation_autoIncomingSubtitle": "Traduz automaticamente mensagens para notificações e para chats ou canais.",
|
||||
"translation_translateMessage": "Traduzir mensagem",
|
||||
"translation_targetLanguage": "Língua-alvo",
|
||||
"translation_useAppLanguage": "Utilize o idioma da aplicação",
|
||||
"translation_downloadedModelLabel": "Modelo baixado",
|
||||
|
||||
@@ -1307,6 +1307,9 @@
|
||||
"translation_title": "Перевод",
|
||||
"translation_enableTitle": "Включить перевод",
|
||||
"translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.",
|
||||
"translation_autoIncomingTitle": "Автоматически переводить сообщения",
|
||||
"translation_autoIncomingSubtitle": "Автоматически переводит сообщения для уведомлений, а также для чатов и каналов.",
|
||||
"translation_translateMessage": "Перевести сообщение",
|
||||
"translation_targetLanguage": "Целевой язык",
|
||||
"translation_useAppLanguage": "Используйте язык приложения",
|
||||
"translation_downloadedModelLabel": "Загруженная модель",
|
||||
|
||||
@@ -2139,6 +2139,9 @@
|
||||
"translation_composerTitle": "Preložte pred odeslaním",
|
||||
"translation_title": "Preklad",
|
||||
"translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.",
|
||||
"translation_autoIncomingTitle": "Automaticky prekladať správy",
|
||||
"translation_autoIncomingSubtitle": "Automaticky prekladá správy pre upozornenia aj pre čet alebo kanál.",
|
||||
"translation_translateMessage": "Preložiť správu",
|
||||
"translation_targetLanguage": "Cieľový jazyk",
|
||||
"translation_useAppLanguage": "Použite jazyk aplikácie",
|
||||
"translation_downloadedModelLabel": "Stiahnutý model",
|
||||
|
||||
@@ -2138,6 +2138,9 @@
|
||||
"translation_enableSubtitle": "Prevedite vstopne sporočila in omogočite predhodno prevajanje.",
|
||||
"translation_enableTitle": "Omogočite prevod",
|
||||
"translation_composerSubtitle": "Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.",
|
||||
"translation_autoIncomingTitle": "Samodejno prevajaj sporočila",
|
||||
"translation_autoIncomingSubtitle": "Samodejno prevaja sporočila za obvestila ter za klepete ali kanale.",
|
||||
"translation_translateMessage": "Prevedi sporočilo",
|
||||
"translation_targetLanguage": "Ciljna jezika",
|
||||
"translation_useAppLanguage": "Uporabite jezik aplikacije",
|
||||
"translation_downloadedModelLabel": "Naložen model",
|
||||
|
||||
@@ -2139,6 +2139,9 @@
|
||||
"translation_title": "Översättning",
|
||||
"translation_composerTitle": "Översätt innan du skickar",
|
||||
"translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.",
|
||||
"translation_autoIncomingTitle": "Översätt meddelanden automatiskt",
|
||||
"translation_autoIncomingSubtitle": "Översätter meddelanden automatiskt för aviseringar och för chattar eller kanaler.",
|
||||
"translation_translateMessage": "Översätt meddelande",
|
||||
"translation_targetLanguage": "Målmedvetet språk",
|
||||
"translation_useAppLanguage": "Använd appens språk",
|
||||
"translation_downloadedModelLabel": "Nedladdad modell",
|
||||
|
||||
@@ -2148,6 +2148,9 @@
|
||||
"translation_enableTitle": "Увімкнути переклад",
|
||||
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
|
||||
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
|
||||
"translation_autoIncomingTitle": "Автоматично перекладати повідомлення",
|
||||
"translation_autoIncomingSubtitle": "Автоматично перекладає повідомлення для сповіщень, а також для чатів і каналів.",
|
||||
"translation_translateMessage": "Перекласти повідомлення",
|
||||
"translation_targetLanguage": "Цільова мова",
|
||||
"translation_useAppLanguage": "Використовувати мову застосунку",
|
||||
"translation_downloadedModelLabel": "Завантажений шаблон",
|
||||
|
||||
@@ -2144,6 +2144,9 @@
|
||||
"translation_composerTitle": "在发送之前进行翻译",
|
||||
"translation_enableTitle": "启用翻译功能",
|
||||
"translation_composerSubtitle": "控制作曲家翻译图标的默认状态。",
|
||||
"translation_autoIncomingTitle": "自动翻译消息",
|
||||
"translation_autoIncomingSubtitle": "自动为通知以及聊天或频道翻译消息。",
|
||||
"translation_translateMessage": "翻译消息",
|
||||
"translation_targetLanguage": "目标语言",
|
||||
"translation_useAppLanguage": "使用应用程序语言",
|
||||
"translation_downloadedModelLabel": "下载的模型",
|
||||
|
||||
+23
-1
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -214,7 +215,10 @@ class MeshCoreApp extends StatelessWidget {
|
||||
// Update notification service with resolved locale
|
||||
final locale = Localizations.localeOf(context);
|
||||
NotificationService().setLocale(locale);
|
||||
return child ?? const SizedBox.shrink();
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: _systemUiOverlayStyle(context),
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||
? const ChromeRequiredScreen()
|
||||
@@ -236,6 +240,24 @@ class MeshCoreApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
SystemUiOverlayStyle _systemUiOverlayStyle(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final iconBrightness = isDark ? Brightness.light : Brightness.dark;
|
||||
|
||||
// Keep Android system bars aligned with the resolved Flutter theme.
|
||||
return SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: iconBrightness,
|
||||
statusBarBrightness: isDark ? Brightness.dark : Brightness.light,
|
||||
systemNavigationBarColor: colorScheme.surface,
|
||||
systemNavigationBarIconBrightness: iconBrightness,
|
||||
systemNavigationBarDividerColor: colorScheme.surface,
|
||||
systemNavigationBarContrastEnforced: false,
|
||||
);
|
||||
}
|
||||
|
||||
Locale? _localeFromSetting(String? languageCode) {
|
||||
if (languageCode == null) return null;
|
||||
return Locale(languageCode);
|
||||
|
||||
@@ -113,6 +113,7 @@ class AppSettings {
|
||||
final int tcpServerPort;
|
||||
final bool jumpToOldestUnread;
|
||||
final bool translationEnabled;
|
||||
final bool autoTranslateIncomingMessages;
|
||||
final String? translationTargetLanguageCode;
|
||||
final bool composerTranslationEnabled;
|
||||
final String? translationModelSourceUrl;
|
||||
@@ -166,6 +167,7 @@ class AppSettings {
|
||||
this.tcpServerPort = 0,
|
||||
this.jumpToOldestUnread = false,
|
||||
this.translationEnabled = false,
|
||||
this.autoTranslateIncomingMessages = true,
|
||||
this.translationTargetLanguageCode,
|
||||
this.composerTranslationEnabled = false,
|
||||
this.translationModelSourceUrl,
|
||||
@@ -226,6 +228,7 @@ class AppSettings {
|
||||
'tcp_server_port': tcpServerPort,
|
||||
'jump_to_oldest_unread': jumpToOldestUnread,
|
||||
'translation_enabled': translationEnabled,
|
||||
'auto_translate_incoming_messages': autoTranslateIncomingMessages,
|
||||
'translation_target_language_code': translationTargetLanguageCode,
|
||||
'composer_translation_enabled': composerTranslationEnabled,
|
||||
'translation_model_source_url': translationModelSourceUrl,
|
||||
@@ -307,6 +310,8 @@ class AppSettings {
|
||||
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
|
||||
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
|
||||
translationEnabled: json['translation_enabled'] as bool? ?? false,
|
||||
autoTranslateIncomingMessages:
|
||||
json['auto_translate_incoming_messages'] as bool? ?? true,
|
||||
translationTargetLanguageCode:
|
||||
json['translation_target_language_code'] as String?,
|
||||
composerTranslationEnabled:
|
||||
@@ -396,6 +401,7 @@ class AppSettings {
|
||||
int? tcpServerPort,
|
||||
bool? jumpToOldestUnread,
|
||||
bool? translationEnabled,
|
||||
bool? autoTranslateIncomingMessages,
|
||||
Object? translationTargetLanguageCode = _unset,
|
||||
bool? composerTranslationEnabled,
|
||||
Object? translationModelSourceUrl = _unset,
|
||||
@@ -453,6 +459,8 @@ class AppSettings {
|
||||
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
||||
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
|
||||
translationEnabled: translationEnabled ?? this.translationEnabled,
|
||||
autoTranslateIncomingMessages:
|
||||
autoTranslateIncomingMessages ?? this.autoTranslateIncomingMessages,
|
||||
translationTargetLanguageCode: translationTargetLanguageCode == _unset
|
||||
? this.translationTargetLanguageCode
|
||||
: translationTargetLanguageCode as String?,
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../services/app_settings_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'map_cache_screen.dart';
|
||||
|
||||
@@ -23,6 +24,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
title: AdaptiveAppBarTitle(context.l10n.appSettings_title),
|
||||
centerTitle: true,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
@@ -559,6 +561,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
TranslationService translationService,
|
||||
) {
|
||||
final settings = settingsService.settings;
|
||||
final translationEnabled = settings.translationEnabled;
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -579,11 +582,41 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.outgoing_mail),
|
||||
title: Text(context.l10n.translation_composerTitle),
|
||||
subtitle: Text(context.l10n.translation_composerSubtitle),
|
||||
secondary: Icon(
|
||||
Icons.auto_awesome_outlined,
|
||||
color: translationEnabled ? null : Colors.grey,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.translation_autoIncomingTitle,
|
||||
style: TextStyle(color: translationEnabled ? null : Colors.grey),
|
||||
),
|
||||
subtitle: Text(
|
||||
context.l10n.translation_autoIncomingSubtitle,
|
||||
style: TextStyle(color: translationEnabled ? null : Colors.grey),
|
||||
),
|
||||
value: settings.autoTranslateIncomingMessages,
|
||||
onChanged: translationEnabled
|
||||
? settingsService.setAutoTranslateIncomingMessages
|
||||
: null,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: Icon(
|
||||
Icons.outgoing_mail,
|
||||
color: translationEnabled ? null : Colors.grey,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.translation_composerTitle,
|
||||
style: TextStyle(color: translationEnabled ? null : Colors.grey),
|
||||
),
|
||||
subtitle: Text(
|
||||
context.l10n.translation_composerSubtitle,
|
||||
style: TextStyle(color: translationEnabled ? null : Colors.grey),
|
||||
),
|
||||
value: settings.composerTranslationEnabled,
|
||||
onChanged: settingsService.setComposerTranslationEnabled,
|
||||
onChanged: translationEnabled
|
||||
? settingsService.setComposerTranslationEnabled
|
||||
: null,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
@@ -34,6 +35,7 @@ import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_translation_button.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import '../widgets/unread_divider.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
@@ -302,6 +304,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
const RadioStatsIconButton(),
|
||||
PopupMenuButton<String>(
|
||||
@@ -1386,6 +1389,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
}
|
||||
|
||||
void _showMessageActions(ChannelMessage message) {
|
||||
final translationService = context.read<TranslationService>();
|
||||
final canTranslateMessage =
|
||||
translationService.canTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: false,
|
||||
isOutgoing: message.isOutgoing,
|
||||
) &&
|
||||
(message.translatedText?.trim().isEmpty ?? true);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
@@ -1427,6 +1439,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_copyMessageText(message.text);
|
||||
},
|
||||
),
|
||||
if (canTranslateMessage)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.translate),
|
||||
title: Text(context.l10n.translation_translateMessage),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
unawaited(
|
||||
context.read<MeshCoreConnector>().translateChannelMessage(
|
||||
widget.channel.index,
|
||||
message,
|
||||
manualTranslation: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!message.isOutgoing)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||
|
||||
@@ -23,6 +23,7 @@ import '../widgets/list_filter_widget.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/qr_code_display.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channel_chat_screen.dart';
|
||||
@@ -103,6 +104,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
title: AppBarTitle(context.l10n.channels_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
@@ -152,12 +154,17 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
await context.read<MeshCoreConnector>().getChannels(force: true);
|
||||
},
|
||||
child: () {
|
||||
if (connector.isLoadingChannels) {
|
||||
final channels = connector.channels;
|
||||
final waitingForFirstChannel =
|
||||
connector.isLoadingChannels && channels.isEmpty;
|
||||
|
||||
// Only block the list while the first channel is actively loading.
|
||||
// If the initial sync aborts, show cached/partial channels instead
|
||||
// of trapping the user behind an idle spinner.
|
||||
if (waitingForFirstChannel) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final channels = connector.channels;
|
||||
|
||||
if (channels.isEmpty) {
|
||||
return ListView(
|
||||
children: [
|
||||
|
||||
@@ -41,6 +41,7 @@ import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_translation_button.dart';
|
||||
import '../widgets/path_selection_dialog.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
@@ -216,6 +217,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
},
|
||||
),
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
@@ -1578,6 +1580,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
void _showMessageActions(Message message, Contact contact) {
|
||||
final translationService = context.read<TranslationService>();
|
||||
final canTranslateMessage =
|
||||
translationService.canTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: message.isCli,
|
||||
isOutgoing: message.isOutgoing,
|
||||
) &&
|
||||
(message.translatedText?.trim().isEmpty ?? true);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
@@ -1611,6 +1622,21 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_copyMessageText(message.text);
|
||||
},
|
||||
),
|
||||
if (canTranslateMessage)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.translate),
|
||||
title: Text(context.l10n.translation_translateMessage),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
unawaited(
|
||||
context.read<MeshCoreConnector>().translateContactMessage(
|
||||
widget.contact.publicKeyHex,
|
||||
message,
|
||||
manualTranslation: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!message.isOutgoing)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||
|
||||
@@ -27,6 +27,7 @@ import '../widgets/empty_state.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/repeater_login_dialog.dart';
|
||||
import '../widgets/room_login_dialog.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channels_screen.dart';
|
||||
@@ -318,6 +319,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle(context.l10n.contacts_title),
|
||||
automaticallyImplyLeading: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
@@ -606,15 +608,14 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) {
|
||||
final viewState = context.watch<UiViewStateService>();
|
||||
final contacts = connector.contacts;
|
||||
final shouldShowStartupSpinner =
|
||||
contacts.isEmpty &&
|
||||
_groups.isEmpty &&
|
||||
final waitingForInitialContacts =
|
||||
connector.isConnected &&
|
||||
(connector.isLoadingContacts ||
|
||||
connector.isLoadingChannels ||
|
||||
connector.selfPublicKey == null);
|
||||
!connector.hasLoadedContacts &&
|
||||
!connector.isLoadingContacts;
|
||||
final waitingForFirstContact =
|
||||
connector.isLoadingContacts && contacts.isEmpty;
|
||||
|
||||
if (shouldShowStartupSpinner) {
|
||||
if (waitingForInitialContacts || waitingForFirstContact) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import '../services/map_tile_cache_service.dart';
|
||||
import '../utils/contact_search.dart';
|
||||
import '../utils/route_transitions.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../icons/los_icon.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
@@ -414,6 +415,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
title: AppBarTitle(context.l10n.map_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
if (!_isBuildingPathTrace)
|
||||
IconButton(
|
||||
|
||||
@@ -11,7 +11,7 @@ import '../utils/app_logger.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/device_tile.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
@@ -46,7 +46,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
_changedNavigation = true;
|
||||
if (mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const ContactsScreen()),
|
||||
MaterialPageRoute(builder: (context) => const ChannelsScreen()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'app_settings_screen.dart';
|
||||
import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
|
||||
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
|
||||
/// to the UI enum range (always 5-8).
|
||||
@@ -67,6 +68,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
indicators: false,
|
||||
subtitle: false,
|
||||
),
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../services/app_settings_service.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
class TcpScreen extends StatefulWidget {
|
||||
@@ -24,7 +24,7 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
late final TextEditingController _portController;
|
||||
late final MeshCoreConnector _connector;
|
||||
late final VoidCallback _connectionListener;
|
||||
bool _navigatedToContacts = false;
|
||||
bool _navigatedToChannels = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -42,20 +42,20 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
_connectionListener = () {
|
||||
if (!mounted) return;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_navigatedToContacts = false;
|
||||
_navigatedToChannels = false;
|
||||
}
|
||||
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.isTcpTransportConnected &&
|
||||
!_navigatedToContacts) {
|
||||
!_navigatedToChannels) {
|
||||
context.read<AppSettingsService>().setTcpServerAddress(
|
||||
_hostController.text,
|
||||
);
|
||||
context.read<AppSettingsService>().setTcpServerPort(
|
||||
int.tryParse(_portController.text) ?? 0,
|
||||
);
|
||||
_navigatedToContacts = true;
|
||||
_navigatedToChannels = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
MaterialPageRoute(builder: (_) => const ChannelsScreen()),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -67,7 +67,7 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
_hostController.dispose();
|
||||
_portController.dispose();
|
||||
_connector.removeListener(_connectionListener);
|
||||
if (!_navigatedToContacts &&
|
||||
if (!_navigatedToChannels &&
|
||||
_connector.activeTransport == MeshCoreTransportType.tcp &&
|
||||
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/cayenne_lpp.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/telemetry_location_map.dart';
|
||||
|
||||
class TelemetryScreen extends StatefulWidget {
|
||||
@@ -344,6 +345,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
|
||||
@@ -11,7 +11,7 @@ import '../utils/platform_info.dart';
|
||||
import '../utils/usb_port_labels.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'scanner_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
|
||||
@@ -25,7 +25,7 @@ class UsbScreen extends StatefulWidget {
|
||||
class _UsbScreenState extends State<UsbScreen> {
|
||||
final List<String> _ports = <String>[];
|
||||
bool _isLoadingPorts = true;
|
||||
bool _navigatedToContacts = false;
|
||||
bool _navigatedToChannels = false;
|
||||
bool _didScheduleInitialLoad = false;
|
||||
Timer? _hotPlugTimer;
|
||||
late final MeshCoreConnector _connector;
|
||||
@@ -41,14 +41,14 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
_connectionListener = () {
|
||||
if (!mounted) return;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_navigatedToContacts = false;
|
||||
_navigatedToChannels = false;
|
||||
}
|
||||
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.isUsbTransportConnected &&
|
||||
!_navigatedToContacts) {
|
||||
_navigatedToContacts = true;
|
||||
!_navigatedToChannels) {
|
||||
_navigatedToChannels = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
MaterialPageRoute(builder: (_) => const ChannelsScreen()),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -72,7 +72,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
_hotPlugTimer?.cancel();
|
||||
_hotPlugTimer = null;
|
||||
_connector.removeListener(_connectionListener);
|
||||
if (!_navigatedToContacts &&
|
||||
if (!_navigatedToChannels &&
|
||||
_connector.activeTransport == MeshCoreTransportType.usb &&
|
||||
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
|
||||
@@ -235,6 +235,12 @@ class AppSettingsService extends ChangeNotifier {
|
||||
await updateSettings(_settings.copyWith(translationEnabled: value));
|
||||
}
|
||||
|
||||
Future<void> setAutoTranslateIncomingMessages(bool value) async {
|
||||
await updateSettings(
|
||||
_settings.copyWith(autoTranslateIncomingMessages: value),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setTranslationTargetLanguageCode(String? value) async {
|
||||
await updateSettings(
|
||||
_settings.copyWith(translationTargetLanguageCode: value),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:llamadart/llamadart.dart';
|
||||
import 'package:flutter_langdetect/flutter_langdetect.dart';
|
||||
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/translation_support.dart';
|
||||
@@ -41,7 +42,10 @@ class TranslationService extends ChangeNotifier {
|
||||
TranslationService(
|
||||
this._appSettingsService, {
|
||||
TranslationFileStore? fileStore,
|
||||
}) : _fileStore = fileStore ?? TranslationFileStore();
|
||||
}) : _fileStore = fileStore ?? TranslationFileStore() {
|
||||
// Initialize langdetect once at service construction.
|
||||
_langDetectInit = initLangDetect();
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
bool _isDownloading = false;
|
||||
@@ -51,6 +55,7 @@ class TranslationService extends ChangeNotifier {
|
||||
LlamaEngine? _engine;
|
||||
String? _loadedModelPath;
|
||||
String? _failedModelPath;
|
||||
Future<void>? _langDetectInit;
|
||||
int _downloadedBytes = 0;
|
||||
int? _downloadTotalBytes;
|
||||
String? _downloadFileName;
|
||||
@@ -84,7 +89,22 @@ class TranslationService extends ChangeNotifier {
|
||||
'en';
|
||||
}
|
||||
|
||||
bool shouldTranslateIncoming({
|
||||
bool shouldAutoTranslateIncoming({
|
||||
required String text,
|
||||
required bool isCli,
|
||||
required bool isOutgoing,
|
||||
}) {
|
||||
if (!_settings.autoTranslateIncomingMessages) {
|
||||
return false;
|
||||
}
|
||||
return canTranslateIncoming(
|
||||
text: text,
|
||||
isCli: isCli,
|
||||
isOutgoing: isOutgoing,
|
||||
);
|
||||
}
|
||||
|
||||
bool canTranslateIncoming({
|
||||
required String text,
|
||||
required bool isCli,
|
||||
required bool isOutgoing,
|
||||
@@ -368,7 +388,9 @@ class TranslationService extends ChangeNotifier {
|
||||
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
|
||||
return null;
|
||||
}
|
||||
final detectedLanguageCode = await detectLanguage(text);
|
||||
final detectedLanguageCode = await detectLanguage(
|
||||
_stripReplyInfoForDetection(text),
|
||||
);
|
||||
if (detectedLanguageCode != null &&
|
||||
detectedLanguageCode == targetLanguageCode) {
|
||||
return const TranslationResult(
|
||||
@@ -409,7 +431,9 @@ class TranslationService extends ChangeNotifier {
|
||||
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
|
||||
return null;
|
||||
}
|
||||
final detectedLanguageCode = await detectLanguage(text);
|
||||
final detectedLanguageCode = await detectLanguage(
|
||||
_stripReplyInfoForDetection(text),
|
||||
);
|
||||
if (detectedLanguageCode != null &&
|
||||
detectedLanguageCode == targetLanguageCode) {
|
||||
return const TranslationResult(
|
||||
@@ -436,7 +460,26 @@ class TranslationService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<String?> detectLanguage(String text) async {
|
||||
return _heuristicLanguageCode(text);
|
||||
try {
|
||||
// Ensure the detector is initialized (constructor starts init).
|
||||
await (_langDetectInit ??= initLangDetect());
|
||||
final code = detect(text);
|
||||
if (code.isEmpty) return null;
|
||||
return code;
|
||||
} catch (error) {
|
||||
_lastError = error.toString();
|
||||
appLogger.warn('Language detection failed: $error');
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _stripReplyInfoForDetection(String text) {
|
||||
final match = RegExp(
|
||||
r'@\[([^\]]+)\]\s+(.+)$',
|
||||
dotAll: true,
|
||||
).firstMatch(text);
|
||||
return match?.group(2) ?? text;
|
||||
}
|
||||
|
||||
Future<String?> _translateText({
|
||||
@@ -518,72 +561,6 @@ class TranslationService extends ChangeNotifier {
|
||||
trimmed.startsWith('r:'));
|
||||
}
|
||||
|
||||
String? _heuristicLanguageCode(String text) {
|
||||
final trimmed = text.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (RegExp(r'[ぁ-んァ-ン]').hasMatch(text)) {
|
||||
return 'ja';
|
||||
}
|
||||
if (RegExp(r'[가-힣]').hasMatch(text)) {
|
||||
return 'ko';
|
||||
}
|
||||
if (RegExp(r'[\u4e00-\u9fff]').hasMatch(text)) {
|
||||
return 'zh';
|
||||
}
|
||||
|
||||
final lower = trimmed.toLowerCase();
|
||||
final patterns = <String, String>{
|
||||
'uk': r'\b(привіт|дякую|будь|ласка|як|де|не|так|це|є|най|ще|може|для)\b',
|
||||
'ru':
|
||||
r'\b(что|это|как|не|да|нет|он|она|они|быть|есть|для|сегодня|если|уже|может)\b',
|
||||
'bg': r'\b(ще|няма|благодаря|моля|това|какво|тук|ние|вие|не|със|за)\b',
|
||||
'de':
|
||||
r'\b(der|die|das|und|ist|nicht|ein|eine|ich|für|mit|auf|zu|auch|als|an|im|am|es|dem|den|sich|von)\b',
|
||||
'en':
|
||||
r'\b(the|and|is|you|for|with|from|not|that|this|have|be|are|was|were|but|can|will|your|what|when|how|they)\b',
|
||||
'es':
|
||||
r'\b(el|la|los|las|es|que|de|en|con|por|para|no|un|una|se|como|su|al|del|está)\b',
|
||||
'fr':
|
||||
r'\b(le|la|les|un|une|et|est|que|qui|pour|dans|pas|avec|sur|ne|vous|il|elle|des|ce|cette|je|tu|nous|vous)\b',
|
||||
'it':
|
||||
r'\b(il|la|lo|un|una|che|di|da|in|per|con|non|si|mi|ti|noi|voi|lui|lei)\b',
|
||||
'pt':
|
||||
r'\b(os|as|que|de|do|da|em|para|com|por|não|uma|um|se|você|também)\b',
|
||||
'nl':
|
||||
r'\b(de|het|een|en|is|niet|dat|wat|je|ik|op|aan|voor|met|als|nog|zijn)\b',
|
||||
'sv':
|
||||
r'\b(och|är|det|att|som|en|på|inte|har|var|men|du|jag|vi|ni|den|detta)\b',
|
||||
'pl':
|
||||
r'\b(na|się|nie|jest|to|że|do|od|dla|czy|tak|ale|ma|jak|on|ona|my)\b',
|
||||
'sk': r'\b(je|na|so|že|do|od|za|si|to|ten|tá|tí|ako|má|nie|som|sa)\b',
|
||||
'sl': r'\b(in|je|na|se|da|za|od|ne|to|ta|so|kako|bo|sem|si)\b',
|
||||
'hu':
|
||||
r'\b(az|és|nem|van|volt|hogy|mit|mire|ki|mi|ez|azért|is|de|ha|te|ő|mi|itt)\b',
|
||||
};
|
||||
|
||||
final scores = <String, int>{};
|
||||
for (final entry in patterns.entries) {
|
||||
scores[entry.key] = RegExp(
|
||||
entry.value,
|
||||
caseSensitive: false,
|
||||
).allMatches(lower).length;
|
||||
}
|
||||
|
||||
final sorted = scores.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
if (sorted.isEmpty || sorted.first.value == 0) {
|
||||
return null;
|
||||
}
|
||||
if (sorted.length > 1 && sorted.first.value == sorted[1].value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sorted.first.key;
|
||||
}
|
||||
|
||||
String _languageLabel(String code) {
|
||||
for (final option in supportedTranslationLanguages) {
|
||||
if (option.code == code) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
|
||||
class SyncProgressAppBarBottom extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
static const double height = 3;
|
||||
|
||||
const SyncProgressAppBarBottom({super.key});
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(height);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final state = _SyncProgressState.fromConnector(connector);
|
||||
if (state == null) return const SizedBox(height: height);
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: LinearProgressIndicator(
|
||||
value: state.value,
|
||||
minHeight: height,
|
||||
color: state.color,
|
||||
backgroundColor: state.color.withValues(alpha: 0.18),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SyncProgressState {
|
||||
final double? value;
|
||||
final Color color;
|
||||
|
||||
const _SyncProgressState({required this.value, required this.color});
|
||||
|
||||
static _SyncProgressState? fromConnector(MeshCoreConnector connector) {
|
||||
if (connector.isLoadingContacts) {
|
||||
return _SyncProgressState(
|
||||
value: connector.contactSyncProgress,
|
||||
color: Colors.red,
|
||||
);
|
||||
}
|
||||
if (connector.isSyncingChannels) {
|
||||
return _SyncProgressState(
|
||||
value: connector.channelSyncProgress / 100,
|
||||
color: Colors.blue,
|
||||
);
|
||||
}
|
||||
if (connector.isShowingQueuedMessageSyncProgress) {
|
||||
return const _SyncProgressState(value: null, color: Colors.green);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user