mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-10 18:47:15 +10:00
sync last dev with cyr2lat
This commit is contained in:
@@ -320,7 +320,7 @@ const int maxPathSize = 64;
|
|||||||
const int pathHashSize = 1;
|
const int pathHashSize = 1;
|
||||||
const int maxNameSize = 32;
|
const int maxNameSize = 32;
|
||||||
const int maxFrameSize = 172;
|
const int maxFrameSize = 172;
|
||||||
const int appProtocolVersion = 3;
|
const int appProtocolVersion = 4;
|
||||||
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
||||||
const int maxTextPayloadBytes = 160;
|
const int maxTextPayloadBytes = 160;
|
||||||
const int _sendTextMsgOverheadBytes =
|
const int _sendTextMsgOverheadBytes =
|
||||||
|
|||||||
@@ -1632,10 +1632,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_markAsUnread => 'Mark as Unread';
|
String get chat_markAsUnread => 'Пометить как непрочитанные';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_newMessages => 'New messages';
|
String get chat_newMessages => 'Новые сообщения';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Открыть ссылку?';
|
String get chat_openLink => 'Открыть ссылку?';
|
||||||
|
|||||||
@@ -376,6 +376,8 @@
|
|||||||
"chat_direct": "Прямой",
|
"chat_direct": "Прямой",
|
||||||
"chat_poiShared": "Точка интереса отправлена",
|
"chat_poiShared": "Точка интереса отправлена",
|
||||||
"chat_unread": "Непрочитанных: {count}",
|
"chat_unread": "Непрочитанных: {count}",
|
||||||
|
"chat_markAsUnread": "Пометить как непрочитанные",
|
||||||
|
"chat_newMessages": "Новые сообщения",
|
||||||
"map_title": "Карта нод",
|
"map_title": "Карта нод",
|
||||||
"map_noNodesWithLocation": "Нет нод с данными о местоположении",
|
"map_noNodesWithLocation": "Нет нод с данными о местоположении",
|
||||||
"map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте",
|
"map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
@@ -304,6 +304,8 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
|
|||||||
class _ChannelMessagePathMapScreenState
|
class _ChannelMessagePathMapScreenState
|
||||||
extends State<ChannelMessagePathMapScreen> {
|
extends State<ChannelMessagePathMapScreen> {
|
||||||
static const double _labelZoomThreshold = 8.5;
|
static const double _labelZoomThreshold = 8.5;
|
||||||
|
static const double _mapMinZoom = 2.0;
|
||||||
|
static const double _mapMaxZoom = 18.0;
|
||||||
|
|
||||||
final MapController _mapController = MapController();
|
final MapController _mapController = MapController();
|
||||||
Uint8List? _selectedPath;
|
Uint8List? _selectedPath;
|
||||||
@@ -330,6 +332,18 @@ class _ChannelMessagePathMapScreenState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_mapController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||||
|
return platform == TargetPlatform.linux ||
|
||||||
|
platform == TargetPlatform.windows ||
|
||||||
|
platform == TargetPlatform.macOS;
|
||||||
|
}
|
||||||
|
|
||||||
double _getPathDistance(List<LatLng> points) {
|
double _getPathDistance(List<LatLng> points) {
|
||||||
double totalDistance = 0.0;
|
double totalDistance = 0.0;
|
||||||
final distanceCalculator = Distance();
|
final distanceCalculator = Distance();
|
||||||
@@ -357,6 +371,70 @@ class _ChannelMessagePathMapScreenState
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _zoomMapBy(double delta) {
|
||||||
|
final camera = _mapController.camera;
|
||||||
|
final nextZoom = (camera.zoom + delta)
|
||||||
|
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||||
|
.toDouble();
|
||||||
|
_mapController.move(camera.center, nextZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetMapView({
|
||||||
|
required LatLng initialCenter,
|
||||||
|
required double initialZoom,
|
||||||
|
required LatLngBounds? bounds,
|
||||||
|
}) {
|
||||||
|
if (bounds != null) {
|
||||||
|
_mapController.fitCamera(
|
||||||
|
CameraFit.bounds(
|
||||||
|
bounds: bounds,
|
||||||
|
padding: const EdgeInsets.all(64),
|
||||||
|
maxZoom: 16,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_mapController.move(initialCenter, initialZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDesktopMapControls({
|
||||||
|
required LatLng initialCenter,
|
||||||
|
required double initialZoom,
|
||||||
|
required LatLngBounds? bounds,
|
||||||
|
}) {
|
||||||
|
return Positioned(
|
||||||
|
left: 16,
|
||||||
|
top: 16,
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
tooltip: 'Zoom in',
|
||||||
|
onPressed: () => _zoomMapBy(1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
tooltip: 'Zoom out',
|
||||||
|
onPressed: () => _zoomMapBy(-1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.my_location),
|
||||||
|
tooltip: 'Center map',
|
||||||
|
onPressed: () => _resetMapView(
|
||||||
|
initialCenter: initialCenter,
|
||||||
|
initialZoom: initialZoom,
|
||||||
|
bounds: bounds,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
@@ -372,6 +450,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
primaryPath,
|
primaryPath,
|
||||||
widget.message.pathVariants,
|
widget.message.pathVariants,
|
||||||
);
|
);
|
||||||
|
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||||
final selectedPathTmp = _resolveSelectedPath(
|
final selectedPathTmp = _resolveSelectedPath(
|
||||||
_selectedPath,
|
_selectedPath,
|
||||||
observedPaths,
|
observedPaths,
|
||||||
@@ -451,10 +530,20 @@ class _ChannelMessagePathMapScreenState
|
|||||||
padding: const EdgeInsets.all(64),
|
padding: const EdgeInsets.all(64),
|
||||||
maxZoom: 16,
|
maxZoom: 16,
|
||||||
),
|
),
|
||||||
minZoom: 2.0,
|
minZoom: _mapMinZoom,
|
||||||
maxZoom: 18.0,
|
maxZoom: _mapMaxZoom,
|
||||||
interactionOptions: InteractionOptions(
|
interactionOptions: InteractionOptions(
|
||||||
flags: ~InteractiveFlag.rotate,
|
flags: ~InteractiveFlag.rotate,
|
||||||
|
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||||
|
cursorKeyboardRotationOptions:
|
||||||
|
CursorKeyboardRotationOptions.disabled(),
|
||||||
|
keyboardOptions: isDesktop
|
||||||
|
? const KeyboardOptions(
|
||||||
|
enableArrowKeysPanning: true,
|
||||||
|
enableWASDPanning: true,
|
||||||
|
enableRFZooming: true,
|
||||||
|
)
|
||||||
|
: const KeyboardOptions.disabled(),
|
||||||
),
|
),
|
||||||
onPositionChanged: (camera, hasGesture) {
|
onPositionChanged: (camera, hasGesture) {
|
||||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||||
@@ -486,6 +575,12 @@ class _ChannelMessagePathMapScreenState
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (isDesktop)
|
||||||
|
_buildDesktopMapControls(
|
||||||
|
initialCenter: initialCenter,
|
||||||
|
initialZoom: initialZoom,
|
||||||
|
bounds: bounds,
|
||||||
|
),
|
||||||
if (observedPaths.length > 1)
|
if (observedPaths.length > 1)
|
||||||
_buildPathSelector(context, observedPaths, selectedIndex, (
|
_buildPathSelector(context, observedPaths, selectedIndex, (
|
||||||
index,
|
index,
|
||||||
|
|||||||
@@ -492,8 +492,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final unread =
|
final unread = connector.getUnreadCountForChannelIndex(
|
||||||
connector.getUnreadCountForChannelIndex(channel.index);
|
channel.index,
|
||||||
|
);
|
||||||
connector.markChannelRead(channel.index);
|
connector.markChannelRead(channel.index);
|
||||||
await Future.delayed(const Duration(milliseconds: 50));
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@@ -1557,7 +1558,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showDismissibleSnackBar(
|
showDismissibleSnackBar(
|
||||||
context,
|
context,
|
||||||
content: Text(context.l10n.channels_channelUpdateFailed('$e')),
|
content: Text(
|
||||||
|
context.l10n.channels_channelUpdateFailed('$e'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1234,8 +1234,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel(context.l10n)),
|
_buildInfoRow(
|
||||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel(context.l10n)),
|
context.l10n.chat_type,
|
||||||
|
contact.typeLabel(context.l10n),
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context.l10n.chat_path,
|
||||||
|
contact.pathLabel(context.l10n),
|
||||||
|
),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context.l10n.contact_lastSeen,
|
context.l10n.contact_lastSeen,
|
||||||
_formatContactLastMessage(contact.lastMessageAt),
|
_formatContactLastMessage(contact.lastMessageAt),
|
||||||
|
|||||||
@@ -932,16 +932,15 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
||||||
} else {
|
} else {
|
||||||
final connector = context.read<MeshCoreConnector>();
|
final connector = context.read<MeshCoreConnector>();
|
||||||
final unread =
|
final unread = connector.getUnreadCountForContactKey(
|
||||||
connector.getUnreadCountForContactKey(contact.publicKeyHex);
|
contact.publicKeyHex,
|
||||||
|
);
|
||||||
connector.markContactRead(contact.publicKeyHex);
|
connector.markContactRead(contact.publicKeyHex);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ChatScreen(
|
builder: (context) =>
|
||||||
contact: contact,
|
ChatScreen(contact: contact, initialUnreadCount: unread),
|
||||||
initialUnreadCount: unread,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -998,8 +997,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
room: room,
|
room: room,
|
||||||
onLogin: (password, isAdmin) {
|
onLogin: (password, isAdmin) {
|
||||||
final connector = context.read<MeshCoreConnector>();
|
final connector = context.read<MeshCoreConnector>();
|
||||||
final unread =
|
final unread = connector.getUnreadCountForContactKey(
|
||||||
connector.getUnreadCountForContactKey(room.publicKeyHex);
|
room.publicKeyHex,
|
||||||
|
);
|
||||||
connector.markContactRead(room.publicKeyHex);
|
connector.markContactRead(room.publicKeyHex);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -1011,10 +1011,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
password: password,
|
password: password,
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
)
|
)
|
||||||
: ChatScreen(
|
: ChatScreen(contact: room, initialUnreadCount: unread),
|
||||||
contact: room,
|
|
||||||
initialUnreadCount: unread,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
@@ -56,8 +57,11 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
static const double _maxAntennaFeet = 400.0;
|
static const double _maxAntennaFeet = 400.0;
|
||||||
static const double _maxAntennaMeters = _maxAntennaFeet / _metersToFeet;
|
static const double _maxAntennaMeters = _maxAntennaFeet / _metersToFeet;
|
||||||
static const double _labelZoomThreshold = 8.5;
|
static const double _labelZoomThreshold = 8.5;
|
||||||
|
static const double _mapMinZoom = 2.0;
|
||||||
|
static const double _mapMaxZoom = 18.0;
|
||||||
|
|
||||||
final LineOfSightService _lineOfSightService = LineOfSightService();
|
final LineOfSightService _lineOfSightService = LineOfSightService();
|
||||||
|
final MapController _mapController = MapController();
|
||||||
|
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
@@ -99,10 +103,85 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_mapController.dispose();
|
||||||
_lineOfSightService.dispose();
|
_lineOfSightService.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||||
|
return platform == TargetPlatform.linux ||
|
||||||
|
platform == TargetPlatform.windows ||
|
||||||
|
platform == TargetPlatform.macOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _zoomMapBy(double delta) {
|
||||||
|
final camera = _mapController.camera;
|
||||||
|
final nextZoom = (camera.zoom + delta)
|
||||||
|
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||||
|
.toDouble();
|
||||||
|
_mapController.move(camera.center, nextZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetMapView({
|
||||||
|
required LatLng initialCenter,
|
||||||
|
required double initialZoom,
|
||||||
|
required LatLngBounds? bounds,
|
||||||
|
}) {
|
||||||
|
if (bounds != null) {
|
||||||
|
_mapController.fitCamera(
|
||||||
|
CameraFit.bounds(
|
||||||
|
bounds: bounds,
|
||||||
|
padding: const EdgeInsets.all(64),
|
||||||
|
maxZoom: 16,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_mapController.move(initialCenter, initialZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDesktopMapControls({
|
||||||
|
required LatLng initialCenter,
|
||||||
|
required double initialZoom,
|
||||||
|
required LatLngBounds? bounds,
|
||||||
|
}) {
|
||||||
|
final screenHeight = MediaQuery.of(context).size.height;
|
||||||
|
final topOffset = _showHud
|
||||||
|
? math.min(screenHeight * 0.52 + 24, screenHeight - 220)
|
||||||
|
: 12.0;
|
||||||
|
return Positioned(
|
||||||
|
top: topOffset,
|
||||||
|
left: 12,
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
tooltip: 'Zoom in',
|
||||||
|
onPressed: () => _zoomMapBy(1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
tooltip: 'Zoom out',
|
||||||
|
onPressed: () => _zoomMapBy(-1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.my_location),
|
||||||
|
tooltip: 'Center map',
|
||||||
|
onPressed: () => _resetMapView(
|
||||||
|
initialCenter: initialCenter,
|
||||||
|
initialZoom: initialZoom,
|
||||||
|
bounds: bounds,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _runLos() async {
|
Future<void> _runLos() async {
|
||||||
final start = _start;
|
final start = _start;
|
||||||
final end = _end;
|
final end = _end;
|
||||||
@@ -325,6 +404,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
? LatLngBounds.fromPoints(mapPoints)
|
? LatLngBounds.fromPoints(mapPoints)
|
||||||
: null;
|
: null;
|
||||||
final initialZoom = mapPoints.length > 1 ? 13.0 : 2.0;
|
final initialZoom = mapPoints.length > 1 ? 13.0 : 2.0;
|
||||||
|
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||||
if (!_didReceivePositionUpdate) {
|
if (!_didReceivePositionUpdate) {
|
||||||
_showMarkerLabels = initialZoom >= _labelZoomThreshold;
|
_showMarkerLabels = initialZoom >= _labelZoomThreshold;
|
||||||
}
|
}
|
||||||
@@ -350,6 +430,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
FlutterMap(
|
FlutterMap(
|
||||||
|
mapController: _mapController,
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: initialCenter,
|
initialCenter: initialCenter,
|
||||||
initialZoom: initialZoom,
|
initialZoom: initialZoom,
|
||||||
@@ -362,7 +443,19 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
),
|
),
|
||||||
interactionOptions: InteractionOptions(
|
interactionOptions: InteractionOptions(
|
||||||
flags: ~InteractiveFlag.rotate,
|
flags: ~InteractiveFlag.rotate,
|
||||||
|
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||||
|
cursorKeyboardRotationOptions:
|
||||||
|
CursorKeyboardRotationOptions.disabled(),
|
||||||
|
keyboardOptions: isDesktop
|
||||||
|
? const KeyboardOptions(
|
||||||
|
enableArrowKeysPanning: true,
|
||||||
|
enableWASDPanning: true,
|
||||||
|
enableRFZooming: true,
|
||||||
|
)
|
||||||
|
: const KeyboardOptions.disabled(),
|
||||||
),
|
),
|
||||||
|
minZoom: _mapMinZoom,
|
||||||
|
maxZoom: _mapMaxZoom,
|
||||||
onLongPress: (_, point) => _addCustomPoint(point),
|
onLongPress: (_, point) => _addCustomPoint(point),
|
||||||
onPositionChanged: (camera, hasGesture) {
|
onPositionChanged: (camera, hasGesture) {
|
||||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||||
@@ -389,6 +482,12 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (isDesktop)
|
||||||
|
_buildDesktopMapControls(
|
||||||
|
initialCenter: initialCenter,
|
||||||
|
initialZoom: initialZoom,
|
||||||
|
bounds: bounds,
|
||||||
|
),
|
||||||
if (_showHud)
|
if (_showHud)
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 12,
|
left: 12,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
@@ -18,6 +19,9 @@ class MapCacheScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MapCacheScreenState extends State<MapCacheScreen> {
|
class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||||
|
static const double _mapMinZoom = 2.0;
|
||||||
|
static const double _mapMaxZoom = 18.0;
|
||||||
|
|
||||||
final MapController _mapController = MapController();
|
final MapController _mapController = MapController();
|
||||||
|
|
||||||
LatLngBounds? _selectedBounds;
|
LatLngBounds? _selectedBounds;
|
||||||
@@ -43,6 +47,61 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||||
|
return platform == TargetPlatform.linux ||
|
||||||
|
platform == TargetPlatform.windows ||
|
||||||
|
platform == TargetPlatform.macOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _zoomMapBy(double delta) {
|
||||||
|
final camera = _mapController.camera;
|
||||||
|
final nextZoom = (camera.zoom + delta)
|
||||||
|
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||||
|
.toDouble();
|
||||||
|
_mapController.move(camera.center, nextZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetMapView() {
|
||||||
|
final bounds = _selectedBounds;
|
||||||
|
if (bounds != null) {
|
||||||
|
_mapController.fitCamera(
|
||||||
|
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_mapController.move(const LatLng(0, 0), 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDesktopMapControls() {
|
||||||
|
return Positioned(
|
||||||
|
top: 12,
|
||||||
|
left: 12,
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
tooltip: 'Zoom in',
|
||||||
|
onPressed: () => _zoomMapBy(1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
tooltip: 'Zoom out',
|
||||||
|
onPressed: () => _zoomMapBy(-1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.my_location),
|
||||||
|
tooltip: 'Center map',
|
||||||
|
onPressed: _resetMapView,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _loadSettings() {
|
void _loadSettings() {
|
||||||
final settings = context.read<AppSettingsService>().settings;
|
final settings = context.read<AppSettingsService>().settings;
|
||||||
final bounds = MapTileCacheService.boundsFromJson(settings.mapCacheBounds);
|
final bounds = MapTileCacheService.boundsFromJson(settings.mapCacheBounds);
|
||||||
@@ -222,6 +281,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
final selectedBounds = _selectedBounds;
|
final selectedBounds = _selectedBounds;
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||||
final progressValue = _estimatedTiles == 0
|
final progressValue = _estimatedTiles == 0
|
||||||
? 0.0
|
? 0.0
|
||||||
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
||||||
@@ -238,11 +298,24 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
children: [
|
children: [
|
||||||
FlutterMap(
|
FlutterMap(
|
||||||
mapController: _mapController,
|
mapController: _mapController,
|
||||||
options: const MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: LatLng(0, 0),
|
initialCenter: const LatLng(0, 0),
|
||||||
initialZoom: 2.0,
|
initialZoom: 2.0,
|
||||||
minZoom: 2.0,
|
minZoom: _mapMinZoom,
|
||||||
maxZoom: 18.0,
|
maxZoom: _mapMaxZoom,
|
||||||
|
interactionOptions: InteractionOptions(
|
||||||
|
flags: ~InteractiveFlag.rotate,
|
||||||
|
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||||
|
cursorKeyboardRotationOptions:
|
||||||
|
CursorKeyboardRotationOptions.disabled(),
|
||||||
|
keyboardOptions: isDesktop
|
||||||
|
? const KeyboardOptions(
|
||||||
|
enableArrowKeysPanning: true,
|
||||||
|
enableWASDPanning: true,
|
||||||
|
enableRFZooming: true,
|
||||||
|
)
|
||||||
|
: const KeyboardOptions.disabled(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
@@ -265,6 +338,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (isDesktop) _buildDesktopMapControls(),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 12,
|
top: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -58,6 +57,8 @@ class MapScreen extends StatefulWidget {
|
|||||||
class _MapScreenState extends State<MapScreen> {
|
class _MapScreenState extends State<MapScreen> {
|
||||||
// Zoom level at which node labels start to appear
|
// Zoom level at which node labels start to appear
|
||||||
static const double _labelZoomThreshold = 14.0;
|
static const double _labelZoomThreshold = 14.0;
|
||||||
|
static const double _mapMinZoom = 2.0;
|
||||||
|
static const double _mapMaxZoom = 18.0;
|
||||||
|
|
||||||
final MapController _mapController = MapController();
|
final MapController _mapController = MapController();
|
||||||
final MapMarkerService _markerService = MapMarkerService();
|
final MapMarkerService _markerService = MapMarkerService();
|
||||||
@@ -150,11 +151,62 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
return zoom.clamp(4.0, 15.0);
|
return zoom.clamp(4.0, 15.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||||
|
return platform == TargetPlatform.linux ||
|
||||||
|
platform == TargetPlatform.windows ||
|
||||||
|
platform == TargetPlatform.macOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _zoomMapBy(double delta) {
|
||||||
|
final camera = _mapController.camera;
|
||||||
|
final nextZoom = (camera.zoom + delta)
|
||||||
|
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||||
|
.toDouble();
|
||||||
|
_mapController.move(camera.center, nextZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDesktopMapControls(
|
||||||
|
BuildContext context, {
|
||||||
|
required LatLng center,
|
||||||
|
required double zoom,
|
||||||
|
required bool hasPathSelector,
|
||||||
|
}) {
|
||||||
|
return Positioned(
|
||||||
|
left: 16,
|
||||||
|
top: hasPathSelector ? null : 16,
|
||||||
|
bottom: hasPathSelector ? 16 : null,
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
tooltip: 'Zoom in',
|
||||||
|
onPressed: () => _zoomMapBy(1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
tooltip: 'Zoom out',
|
||||||
|
onPressed: () => _zoomMapBy(-1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.my_location),
|
||||||
|
tooltip: 'Center map',
|
||||||
|
onPressed: () => _mapController.move(center, zoom),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
|
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
|
||||||
builder: (context, connector, settingsService, pathHistory, child) {
|
builder: (context, connector, settingsService, pathHistory, child) {
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
|
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||||
final settings = settingsService.settings;
|
final settings = settingsService.settings;
|
||||||
final allContacts = connector.allContacts;
|
final allContacts = connector.allContacts;
|
||||||
|
|
||||||
@@ -451,10 +503,20 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: center,
|
initialCenter: center,
|
||||||
initialZoom: initialZoom,
|
initialZoom: initialZoom,
|
||||||
minZoom: 2.0,
|
minZoom: _mapMinZoom,
|
||||||
maxZoom: 18.0,
|
maxZoom: _mapMaxZoom,
|
||||||
interactionOptions: InteractionOptions(
|
interactionOptions: InteractionOptions(
|
||||||
flags: ~InteractiveFlag.rotate,
|
flags: ~InteractiveFlag.rotate,
|
||||||
|
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||||
|
cursorKeyboardRotationOptions:
|
||||||
|
CursorKeyboardRotationOptions.disabled(),
|
||||||
|
keyboardOptions: isDesktop
|
||||||
|
? const KeyboardOptions(
|
||||||
|
enableArrowKeysPanning: true,
|
||||||
|
enableWASDPanning: true,
|
||||||
|
enableRFZooming: true,
|
||||||
|
)
|
||||||
|
: const KeyboardOptions.disabled(),
|
||||||
),
|
),
|
||||||
onTap: (_, latLng) {
|
onTap: (_, latLng) {
|
||||||
if (_isSelectingPoi) {
|
if (_isSelectingPoi) {
|
||||||
@@ -598,6 +660,13 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
sharedMarkers.length,
|
sharedMarkers.length,
|
||||||
guessedLocations.length,
|
guessedLocations.length,
|
||||||
),
|
),
|
||||||
|
if (isDesktop)
|
||||||
|
_buildDesktopMapControls(
|
||||||
|
context,
|
||||||
|
center: center,
|
||||||
|
zoom: initialZoom,
|
||||||
|
hasPathSelector: _isBuildingPathTrace,
|
||||||
|
),
|
||||||
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1480,8 +1549,14 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoRow(context.l10n.map_type, contact.typeLabel(context.l10n)),
|
_buildInfoRow(
|
||||||
_buildInfoRow(context.l10n.map_path, contact.pathLabel(context.l10n)),
|
context.l10n.map_type,
|
||||||
|
contact.typeLabel(context.l10n),
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
context.l10n.map_path,
|
||||||
|
contact.pathLabel(context.l10n),
|
||||||
|
),
|
||||||
if (contact.hasLocation)
|
if (contact.hasLocation)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context.l10n.map_location,
|
context.l10n.map_location,
|
||||||
|
|||||||
@@ -76,9 +76,12 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||||
static const double _labelZoomThreshold = 8.5;
|
static const double _labelZoomThreshold = 8.5;
|
||||||
|
static const double _mapMinZoom = 2.0;
|
||||||
|
static const double _mapMaxZoom = 18.0;
|
||||||
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
|
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
|
||||||
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
|
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
|
||||||
|
|
||||||
|
final MapController _mapController = MapController();
|
||||||
StreamSubscription<Uint8List>? _frameSubscription;
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
Timer? _timeoutTimer;
|
Timer? _timeoutTimer;
|
||||||
|
|
||||||
@@ -116,11 +119,74 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_mapController.dispose();
|
||||||
_frameSubscription?.cancel();
|
_frameSubscription?.cancel();
|
||||||
_timeoutTimer?.cancel();
|
_timeoutTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||||
|
return platform == TargetPlatform.linux ||
|
||||||
|
platform == TargetPlatform.windows ||
|
||||||
|
platform == TargetPlatform.macOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _zoomMapBy(double delta) {
|
||||||
|
final camera = _mapController.camera;
|
||||||
|
final nextZoom = (camera.zoom + delta)
|
||||||
|
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||||
|
.toDouble();
|
||||||
|
_mapController.move(camera.center, nextZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetMapView() {
|
||||||
|
final bounds = _bounds;
|
||||||
|
if (bounds != null) {
|
||||||
|
_mapController.fitCamera(
|
||||||
|
CameraFit.bounds(
|
||||||
|
bounds: bounds,
|
||||||
|
padding: const EdgeInsets.all(64),
|
||||||
|
maxZoom: 16,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final center = _initialCenter;
|
||||||
|
if (center != null) {
|
||||||
|
_mapController.move(center, _initialZoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDesktopMapControls() {
|
||||||
|
return Positioned(
|
||||||
|
top: 16,
|
||||||
|
left: 16,
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
tooltip: 'Zoom in',
|
||||||
|
onPressed: () => _zoomMapBy(1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
tooltip: 'Zoom out',
|
||||||
|
onPressed: () => _zoomMapBy(-1),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.my_location),
|
||||||
|
tooltip: 'Center map',
|
||||||
|
onPressed: _resetMapView,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Uint8List buildPath(Uint8List pathBytes) {
|
Uint8List buildPath(Uint8List pathBytes) {
|
||||||
Uint8List traceBytes;
|
Uint8List traceBytes;
|
||||||
|
|
||||||
@@ -519,6 +585,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
if (_hasData)
|
if (_hasData)
|
||||||
_buildMapPathTrace(context, tileCache, _targetContact),
|
_buildMapPathTrace(context, tileCache, _targetContact),
|
||||||
|
if (_hasData && _isDesktopPlatform(defaultTargetPlatform))
|
||||||
|
_buildDesktopMapControls(),
|
||||||
if (_points.isEmpty &&
|
if (_points.isEmpty &&
|
||||||
!_hasData &&
|
!_hasData &&
|
||||||
!_isLoading &&
|
!_isLoading &&
|
||||||
@@ -801,10 +869,24 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
MapTileCacheService tileCache,
|
MapTileCacheService tileCache,
|
||||||
Contact? target,
|
Contact? target,
|
||||||
) {
|
) {
|
||||||
|
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||||
return FlutterMap(
|
return FlutterMap(
|
||||||
key: _mapKey,
|
key: _mapKey,
|
||||||
|
mapController: _mapController,
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
|
interactionOptions: InteractionOptions(
|
||||||
|
flags: ~InteractiveFlag.rotate,
|
||||||
|
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||||
|
cursorKeyboardRotationOptions:
|
||||||
|
CursorKeyboardRotationOptions.disabled(),
|
||||||
|
keyboardOptions: isDesktop
|
||||||
|
? const KeyboardOptions(
|
||||||
|
enableArrowKeysPanning: true,
|
||||||
|
enableWASDPanning: true,
|
||||||
|
enableRFZooming: true,
|
||||||
|
)
|
||||||
|
: const KeyboardOptions.disabled(),
|
||||||
|
),
|
||||||
initialCenter: _initialCenter!,
|
initialCenter: _initialCenter!,
|
||||||
initialZoom: _initialZoom,
|
initialZoom: _initialZoom,
|
||||||
initialCameraFit: _bounds == null
|
initialCameraFit: _bounds == null
|
||||||
@@ -814,8 +896,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
padding: const EdgeInsets.all(64),
|
padding: const EdgeInsets.all(64),
|
||||||
maxZoom: 16,
|
maxZoom: 16,
|
||||||
),
|
),
|
||||||
minZoom: 2.0,
|
minZoom: _mapMinZoom,
|
||||||
maxZoom: 18.0,
|
maxZoom: _mapMaxZoom,
|
||||||
onPositionChanged: (camera, hasGesture) {
|
onPositionChanged: (camera, hasGesture) {
|
||||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||||
if (shouldShow != _showNodeLabels && mounted) {
|
if (shouldShow != _showNodeLabels && mounted) {
|
||||||
|
|||||||
@@ -65,8 +65,7 @@ class BackgroundService {
|
|||||||
return AppLocalizations.delegate.load(overrideLocale);
|
return AppLocalizations.delegate.load(overrideLocale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final preferred =
|
final preferred = WidgetsBinding.instance.platformDispatcher.locales;
|
||||||
WidgetsBinding.instance.platformDispatcher.locales;
|
|
||||||
final match = basicLocaleListResolution(preferred, supported);
|
final match = basicLocaleListResolution(preferred, supported);
|
||||||
return AppLocalizations.delegate.load(match);
|
return AppLocalizations.delegate.load(match);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,12 +519,11 @@ class TranslationService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? _heuristicLanguageCode(String text) {
|
String? _heuristicLanguageCode(String text) {
|
||||||
if (RegExp(r'[іїєґІЇЄҐ]').hasMatch(text)) {
|
final trimmed = text.trim();
|
||||||
return 'uk';
|
if (trimmed.isEmpty) {
|
||||||
}
|
return null;
|
||||||
if (RegExp(r'[а-яёА-ЯЁ]').hasMatch(text)) {
|
|
||||||
return 'ru';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RegExp(r'[ぁ-んァ-ン]').hasMatch(text)) {
|
if (RegExp(r'[ぁ-んァ-ン]').hasMatch(text)) {
|
||||||
return 'ja';
|
return 'ja';
|
||||||
}
|
}
|
||||||
@@ -534,9 +533,55 @@ class TranslationService extends ChangeNotifier {
|
|||||||
if (RegExp(r'[\u4e00-\u9fff]').hasMatch(text)) {
|
if (RegExp(r'[\u4e00-\u9fff]').hasMatch(text)) {
|
||||||
return 'zh';
|
return 'zh';
|
||||||
}
|
}
|
||||||
// Latin-script languages can't be reliably distinguished by characters
|
|
||||||
// alone — return null so the translator always attempts translation.
|
final lower = trimmed.toLowerCase();
|
||||||
return null;
|
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) {
|
String _languageLabel(String code) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
flserial
|
flserial
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
@@ -36,10 +36,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('repeater', () {
|
test('repeater', () {
|
||||||
expect(
|
expect(_contact(type: advTypeRepeater).typeLabel(l10n), 'Repeater');
|
||||||
_contact(type: advTypeRepeater).typeLabel(l10n),
|
|
||||||
'Repeater',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('room', () {
|
test('room', () {
|
||||||
@@ -57,24 +54,15 @@ void main() {
|
|||||||
|
|
||||||
group('Contact.pathLabel (override)', () {
|
group('Contact.pathLabel (override)', () {
|
||||||
test('override < 0 -> flood (forced)', () {
|
test('override < 0 -> flood (forced)', () {
|
||||||
expect(
|
expect(_contact(pathOverride: -1).pathLabel(l10n), 'Flood (forced)');
|
||||||
_contact(pathOverride: -1).pathLabel(l10n),
|
|
||||||
'Flood (forced)',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('override == 0 -> direct (forced)', () {
|
test('override == 0 -> direct (forced)', () {
|
||||||
expect(
|
expect(_contact(pathOverride: 0).pathLabel(l10n), 'Direct (forced)');
|
||||||
_contact(pathOverride: 0).pathLabel(l10n),
|
|
||||||
'Direct (forced)',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('override > 0 -> hops (forced)', () {
|
test('override > 0 -> hops (forced)', () {
|
||||||
expect(
|
expect(_contact(pathOverride: 3).pathLabel(l10n), '3 hops (forced)');
|
||||||
_contact(pathOverride: 3).pathLabel(l10n),
|
|
||||||
'3 hops (forced)',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('override takes precedence over pathLength', () {
|
test('override takes precedence over pathLength', () {
|
||||||
|
|||||||
@@ -54,11 +54,6 @@
|
|||||||
"chat_newMessages"
|
"chat_newMessages"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
|
||||||
"chat_markAsUnread",
|
|
||||||
"chat_newMessages"
|
|
||||||
],
|
|
||||||
|
|
||||||
"sk": [
|
"sk": [
|
||||||
"chat_markAsUnread",
|
"chat_markAsUnread",
|
||||||
"chat_newMessages"
|
"chat_newMessages"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
flserial
|
flserial
|
||||||
flutter_local_notifications_windows
|
flutter_local_notifications_windows
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
Reference in New Issue
Block a user