Added Cyr2Lat compression by replacing 2-byte cyrillic chars by 1-byte latin

This commit is contained in:
HDDen
2026-04-22 03:59:43 +03:00
parent 6b6a881c7a
commit 609d0c8dbc
47 changed files with 1350 additions and 12 deletions
+113
View File
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -60,6 +61,8 @@ class AppSettingsScreen extends StatelessWidget {
const SizedBox(height: 16),
_buildMapSettingsCard(context, settingsService),
const SizedBox(height: 16),
_buildCyr2LatCard(context, settingsService),
const SizedBox(height: 16),
_buildDebugCard(context, settingsService),
],
);
@@ -1255,6 +1258,116 @@ class AppSettingsScreen extends StatelessWidget {
return '${sizeMb.toStringAsFixed(1)} MB • $source';
}
Widget _buildCyr2LatCard(
BuildContext context,
AppSettingsService settingsService,
) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
context.l10n.channels_cyr2latSettingsHeading,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
ListTile(
leading: const Icon(Icons.translate),
title: Text(context.l10n.channels_cyr2latSettingsSubheading),
subtitle: Text(context.l10n.channels_cyr2latSettingsDscr),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showCyr2LatDialog(context, settingsService),
),
],
),
);
}
void _showCyr2LatDialog(
BuildContext context,
AppSettingsService settingsService,
) {
final controller = TextEditingController(
text: const JsonEncoder.withIndent(
' ',
).convert(settingsService.settings.cyr2latCharMap),
);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.channels_cyr2latSettingsDscr),
content: SingleChildScrollView(
child: TextField(
controller: controller,
maxLines: 20,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: context.l10n.channels_cyr2latSettingsDialogHint,
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () {
try {
final json =
jsonDecode(controller.text) as Map<String, dynamic>;
final map = json.map(
(key, value) => MapEntry(key, value.toString()),
);
final newSettings = settingsService.settings.copyWith(
cyr2latCharMap: map,
);
settingsService.updateSettings(newSettings);
Navigator.pop(context);
showDismissibleSnackBar(
context,
content: Text(
context.l10n.channels_cyr2latSettingsDialogSuccess,
),
);
} catch (e) {
showDismissibleSnackBar(
context,
content: Text(
context.l10n.channels_cyr2latSettingsDialogWrongJSON(
e.toString(),
),
),
);
}
},
child: Text(context.l10n.common_save),
),
TextButton(
onPressed: () {
final newSettings = settingsService.settings.copyWith(
cyr2latCharMap: defaultCyr2LatCharMap,
);
settingsService.updateSettings(newSettings);
Navigator.pop(context);
showDismissibleSnackBar(
context,
content: Text(
context.l10n.channels_cyr2latSettingsDialogResetted,
),
);
},
child: Text(context.l10n.channels_cyr2latSettingsDialogReset),
),
],
),
);
}
Widget _buildDebugCard(
BuildContext context,
AppSettingsService settingsService,
+16 -1
View File
@@ -11,6 +11,7 @@ import '../connector/meshcore_connector.dart';
import '../utils/platform_info.dart';
import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/cyr2lat.dart';
import '../helpers/gif_helper.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/snack_bar_builder.dart';
@@ -1100,7 +1101,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
hintText: context.l10n.chat_typeMessage,
onSubmitted: (_) => _sendMessage(),
encoder:
connector.isChannelSmazEnabled(widget.channel.index)
(connector.isChannelSmazEnabled(
widget.channel.index,
) ||
connector.isChannelCyr2LatEnabled(
widget.channel.index,
))
? (text) => connector.prepareChannelOutboundText(
widget.channel.index,
text,
@@ -1213,6 +1219,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return;
}
// When messageText is transformed with cyr2lat, it (generally) hasn't visual differences,
// but we getting messages doubles in chat screen (source text and transformed).
// To prevent, we'll perform transform of source before pass to main sender logic.
// We can pass whole text, senderName will be kept intact
if (connector.isChannelCyr2LatEnabled(widget.channel.index)) {
messageText = Cyr2Lat.encode(messageText);
}
// end transform
_textController.clear();
_cancelReply();
_textFieldFocusNode.requestFocus();
+25 -1
View File
@@ -1404,6 +1404,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final nameController = TextEditingController(text: channel.name);
final pskController = TextEditingController(text: channel.pskHex);
bool smazEnabled = connector.isChannelSmazEnabled(channel.index);
bool cyr2latEnabled = connector.isChannelCyr2LatEnabled(channel.index);
showDialog(
context: context,
@@ -1449,7 +1450,26 @@ class _ChannelsScreenState extends State<ChannelsScreen>
contentPadding: EdgeInsets.zero,
title: Text(dialogContext.l10n.channels_smazCompression),
value: smazEnabled,
onChanged: (value) => setState(() => smazEnabled = value),
onChanged: (value) => setState(() {
smazEnabled = value;
if (smazEnabled) {
cyr2latEnabled = false;
}
}),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(dialogContext.l10n.channels_cyr2latCompression),
subtitle: Text(
dialogContext.l10n.channels_cyr2latCompressionDscr,
),
value: cyr2latEnabled,
onChanged: (value) => setState(() {
cyr2latEnabled = value;
if (cyr2latEnabled) {
smazEnabled = false;
}
}),
),
],
),
@@ -1482,6 +1502,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
channel.index,
smazEnabled,
);
await connector.setChannelCyr2LatEnabled(
channel.index,
cyr2latEnabled,
);
if (!context.mounted) return;
showDismissibleSnackBar(
context,
+56 -4
View File
@@ -13,6 +13,7 @@ import 'package:latlong2/latlong.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/cyr2lat.dart';
import '../helpers/reaction_helper.dart';
import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart';
@@ -574,9 +575,12 @@ class _ChatScreenState extends State<ChatScreen> {
hintText: context.l10n.chat_typeMessage,
onSubmitted: (_) => _sendMessage(connector),
encoder:
connector.isContactSmazEnabled(
widget.contact.publicKeyHex,
)
(connector.isContactSmazEnabled(
widget.contact.publicKeyHex,
) ||
connector.isContactCyr2LatEnabled(
widget.contact.publicKeyHex,
))
? (text) => connector.prepareContactOutboundText(
widget.contact,
text,
@@ -695,6 +699,18 @@ class _ChatScreenState extends State<ChatScreen> {
return;
}
// This is only for cyr2lat compression - to see the message being sent in the same format as the other person will receive
try {
if (connector.isContactCyr2LatEnabled(
_resolveContact(connector).publicKeyHex,
)) {
outgoingText = Cyr2Lat.encode(outgoingText);
}
} catch (_) {
// TODO maybe log
}
// end transform
_textController.clear();
_textFieldFocusNode.requestFocus();
connector.sendMessage(
@@ -1196,8 +1212,12 @@ class _ChatScreenState extends State<ChatScreen> {
void _showContactSettings(BuildContext context) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
connector.ensureContactCyr2LatSettingLoaded(widget.contact.publicKeyHex);
final contact = widget.contact;
bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex);
bool cyr2latEnabled = connector.isContactCyr2LatEnabled(
contact.publicKeyHex,
);
bool teleBaseEnabled = contact.teleBaseEnabled;
bool teleLocEnabled = contact.teleLocEnabled;
bool teleEnvEnabled = contact.teleEnvEnabled;
@@ -1228,7 +1248,39 @@ class _ChatScreenState extends State<ChatScreen> {
contact.publicKeyHex,
value,
);
setDialogState(() => smazEnabled = value);
connector.setContactCyr2LatEnabled(
contact.publicKeyHex,
false,
);
setDialogState(() {
smazEnabled = value;
if (smazEnabled) {
cyr2latEnabled = false;
}
});
},
),
const Divider(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.channels_cyr2latCompression),
subtitle: Text(context.l10n.channels_cyr2latCompressionDscr),
value: cyr2latEnabled,
onChanged: (value) {
connector.setContactCyr2LatEnabled(
contact.publicKeyHex,
value,
);
connector.setContactSmazEnabled(
contact.publicKeyHex,
false,
);
setDialogState(() {
cyr2latEnabled = value;
if (cyr2latEnabled) {
smazEnabled = false;
}
});
},
),
const Divider(height: 8),