mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-22 18:34:29 +10:00
Merge branch 'dev' into telemetry-gps-map
This commit is contained in:
@@ -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((_) {
|
||||
|
||||
Reference in New Issue
Block a user