Merge branch 'dev' into telemetry-gps-map

This commit is contained in:
HDDen
2026-05-26 20:58:57 +03:00
55 changed files with 884 additions and 236 deletions
+37 -4
View File
@@ -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(
+27
View File
@@ -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),
+10 -3
View File
@@ -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: [
+26
View File
@@ -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),
+8 -7
View File
@@ -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());
}
+2
View File
@@ -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(
+2 -2
View File
@@ -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()),
);
}
}
+2
View File
@@ -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,
+7 -7
View File
@@ -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((_) {
+2
View File
@@ -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),
+7 -7
View File
@@ -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((_) {