mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-11 02:57:04 +10:00
Merge branch 'dev' into pr-404-merge
This commit is contained in:
@@ -87,6 +87,7 @@ keystore.properties
|
|||||||
# IDE
|
# IDE
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
.contextstream/
|
||||||
|
|
||||||
# Cloudflare Wrangler
|
# Cloudflare Wrangler
|
||||||
.wrangler
|
.wrangler
|
||||||
|
|||||||
@@ -658,6 +658,27 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setContactUnreadCount(String contactKeyHex, int count) {
|
||||||
|
_contactUnreadCount[contactKeyHex] = count;
|
||||||
|
_unreadStore.saveContactUnreadCount(
|
||||||
|
Map<String, int>.from(_contactUnreadCount),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setChannelUnreadCount(int channelIndex, int count) {
|
||||||
|
final channel = _findChannelByIndex(channelIndex);
|
||||||
|
if (channel != null) {
|
||||||
|
channel.unreadCount = count;
|
||||||
|
unawaited(
|
||||||
|
_channelStore.saveChannels(
|
||||||
|
_channels.isNotEmpty ? _channels : _cachedChannels,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void markChannelRead(int channelIndex) {
|
void markChannelRead(int channelIndex) {
|
||||||
final channel = _findChannelByIndex(channelIndex);
|
final channel = _findChannelByIndex(channelIndex);
|
||||||
if (channel != null && channel.unreadCount > 0) {
|
if (channel != null && channel.unreadCount > 0) {
|
||||||
|
|||||||
@@ -49,6 +49,25 @@ class ChatScrollController extends ScrollController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Jumps toward an off-screen message so that lazy ListView.builder builds
|
||||||
|
/// items near it. Only visible + cacheExtent items have real heights, so we
|
||||||
|
/// use proportion of maxScrollExtent (itself an estimate from built items'
|
||||||
|
/// avg height). Call [onJumped] on the next frame to ensureVisible/scroll
|
||||||
|
/// to the exact target.
|
||||||
|
void jumpToEstimatedOffset({
|
||||||
|
required int unreadCount,
|
||||||
|
required int totalMessages,
|
||||||
|
required VoidCallback onJumped,
|
||||||
|
}) {
|
||||||
|
if (!hasClients || totalMessages == 0) return;
|
||||||
|
final maxExtent = position.maxScrollExtent;
|
||||||
|
final jumpOffset = maxExtent * (unreadCount / totalMessages);
|
||||||
|
if (jumpOffset > 100) {
|
||||||
|
jumpTo(jumpOffset);
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => onJumped());
|
||||||
|
}
|
||||||
|
|
||||||
void scrollToBottomIfAtBottom() {
|
void scrollToBottomIfAtBottom() {
|
||||||
// Only scroll if jump button is NOT showing (i.e., already at bottom)
|
// Only scroll if jump button is NOT showing (i.e., already at bottom)
|
||||||
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
|
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
|
||||||
|
|||||||
+42
-1
@@ -2108,5 +2108,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "Множество потвърждения"
|
"settings_multiAck": "Множество потвърждения",
|
||||||
|
"map_sharedAt": "Споделено",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losSelectedObstructionTitle": "Избрано препятствие",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losBlockedSpotsHint": "Кликнете върху блокираната точка, за да я отбележите на картата.",
|
||||||
|
"losBlockedSpotsTitle": "Ограничени места",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2136,5 +2136,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "Mehrere Bestätigungen"
|
"settings_multiAck": "Mehrere Bestätigungen",
|
||||||
|
"map_sharedAt": "Geteilt",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsTitle": "Reservierte Plätze",
|
||||||
|
"losSelectedObstructionTitle": "Ausgewählte Behinderung",
|
||||||
|
"losBlockedSpotChip": "{distance} • {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losBlockedSpotsHint": "Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.",
|
||||||
|
"losSelectedObstructionDetails": "Blockiert durch {obstruction} in einer Höhe von {heightUnit}, {distanceFromA} von A und {distanceFromB} von B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -829,6 +829,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"chat_markAsUnread": "Mark as Unread",
|
||||||
|
"chat_newMessages": "New messages",
|
||||||
"chat_openLink": "Open Link?",
|
"chat_openLink": "Open Link?",
|
||||||
"chat_openLinkConfirmation": "Do you want to open this link in your browser?",
|
"chat_openLinkConfirmation": "Do you want to open this link in your browser?",
|
||||||
"chat_open": "Open",
|
"chat_open": "Open",
|
||||||
@@ -914,6 +916,7 @@
|
|||||||
"map_guessedLocation": "Guessed location",
|
"map_guessedLocation": "Guessed location",
|
||||||
"map_lastSeenTime": "Last Seen Time",
|
"map_lastSeenTime": "Last Seen Time",
|
||||||
"map_sharedPin": "Shared pin",
|
"map_sharedPin": "Shared pin",
|
||||||
|
"map_sharedAt": "Shared",
|
||||||
"map_joinRoom": "Join Room",
|
"map_joinRoom": "Join Room",
|
||||||
"map_manageRepeater": "Manage Repeater",
|
"map_manageRepeater": "Manage Repeater",
|
||||||
"map_tapToAdd": "Tap on nodes to add them to the path.",
|
"map_tapToAdd": "Tap on nodes to add them to the path.",
|
||||||
@@ -1893,6 +1896,46 @@
|
|||||||
"losLegendRadioHorizon": "Radio horizon",
|
"losLegendRadioHorizon": "Radio horizon",
|
||||||
"losLegendLosBeam": "LOS beam",
|
"losLegendLosBeam": "LOS beam",
|
||||||
"losLegendTerrain": "Terrain",
|
"losLegendTerrain": "Terrain",
|
||||||
|
"losBlockedSpotsTitle": "Blocked spots",
|
||||||
|
"losBlockedSpotsHint": "Tap a blocked spot to highlight it on the map.",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losSelectedObstructionTitle": "Selected obstruction",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"losFrequencyLabel": "Frequency",
|
"losFrequencyLabel": "Frequency",
|
||||||
"losFrequencyInfoTooltip": "View calculation details",
|
"losFrequencyInfoTooltip": "View calculation details",
|
||||||
"losFrequencyDialogTitle": "Radio horizon calculation",
|
"losFrequencyDialogTitle": "Radio horizon calculation",
|
||||||
|
|||||||
+42
-1
@@ -2136,5 +2136,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "Múltiples respuestas de confirmación"
|
"settings_multiAck": "Múltiples respuestas de confirmación",
|
||||||
|
"map_sharedAt": "Compartido",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsTitle": "Espacios ocupados",
|
||||||
|
"losBlockedSpotsHint": "Seleccione un punto bloqueado para resaltarlo en el mapa.",
|
||||||
|
"losSelectedObstructionTitle": "Obstrucción seleccionada",
|
||||||
|
"losSelectedObstructionDetails": "Bloqueado por {obstruction} a una altura de {heightUnit}, a {distanceFromA} metros de A y a {distanceFromB} metros de B ({distanceUnit}).",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2108,5 +2108,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "Plusieurs accusés de réception"
|
"settings_multiAck": "Plusieurs accusés de réception",
|
||||||
|
"map_sharedAt": "Partagé",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losSelectedObstructionTitle": "Obstruction sélectionnée",
|
||||||
|
"losBlockedSpotsTitle": "Places occupés",
|
||||||
|
"losBlockedSpotsHint": "Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.",
|
||||||
|
"losSelectedObstructionDetails": "Bloqué par {obstruction}, à une hauteur de {heightUnit}, à une distance de {distanceFromA} par rapport à A et à une distance de {distanceFromB} par rapport à B ({distanceUnit}).",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2146,5 +2146,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "Többszörös visszaigazolások"
|
"settings_multiAck": "Többszörös visszaigazolások",
|
||||||
|
"map_sharedAt": "Megosztva",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losSelectedObstructionTitle": "Kiválasztott akadály",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losBlockedSpotsHint": "A blokkolt területet megjelölve, hogy a térképen kiemeljük.",
|
||||||
|
"losBlockedSpotsTitle": "Foglalhatatlan területek",
|
||||||
|
"losSelectedObstructionDetails": "Elakadt a {obstruction} miatt, {heightUnit} magasságban, {distanceFromA} méterrel A-tól és {distanceFromB} méterrel B-től ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2108,5 +2108,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "ACK multipli"
|
"settings_multiAck": "ACK multipli",
|
||||||
|
"map_sharedAt": "Condiviso",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losSelectedObstructionTitle": "Ostacolo selezionato",
|
||||||
|
"losBlockedSpotsHint": "Tocca un punto bloccato sulla mappa per evidenziarlo.",
|
||||||
|
"losBlockedSpotsTitle": "Posti occupati",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2146,5 +2146,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "複数のACK(応答)"
|
"settings_multiAck": "複数のACK(応答)",
|
||||||
|
"map_sharedAt": "共有済み",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsHint": "地図上で、特定された場所を強調するために、該当する場所をタップしてください。",
|
||||||
|
"losSelectedObstructionTitle": "選択された障害",
|
||||||
|
"losBlockedSpotsTitle": "利用できない場所",
|
||||||
|
"losSelectedObstructionDetails": "{obstruction} によって {heightUnit} の高さで、A地点から {distanceFromA}、B地点から {distanceFromB} ({distanceUnit}) の距離で塞がれています。",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2146,5 +2146,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "다중 ACK"
|
"settings_multiAck": "다중 ACK",
|
||||||
|
"map_sharedAt": "공유됨",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsHint": "지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.",
|
||||||
|
"losBlockedSpotsTitle": "차단된 공간",
|
||||||
|
"losSelectedObstructionTitle": "선택된 장애물",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2854,6 +2854,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Unread: {count}'**
|
/// **'Unread: {count}'**
|
||||||
String chat_unread(int count);
|
String chat_unread(int count);
|
||||||
|
|
||||||
|
/// No description provided for @chat_markAsUnread.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Mark as Unread'**
|
||||||
|
String get chat_markAsUnread;
|
||||||
|
|
||||||
|
/// No description provided for @chat_newMessages.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New messages'**
|
||||||
|
String get chat_newMessages;
|
||||||
|
|
||||||
/// No description provided for @chat_openLink.
|
/// No description provided for @chat_openLink.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -3196,6 +3208,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Shared pin'**
|
/// **'Shared pin'**
|
||||||
String get map_sharedPin;
|
String get map_sharedPin;
|
||||||
|
|
||||||
|
/// No description provided for @map_sharedAt.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Shared'**
|
||||||
|
String get map_sharedAt;
|
||||||
|
|
||||||
/// No description provided for @map_joinRoom.
|
/// No description provided for @map_joinRoom.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -5706,6 +5724,47 @@ abstract class AppLocalizations {
|
|||||||
/// **'Terrain'**
|
/// **'Terrain'**
|
||||||
String get losLegendTerrain;
|
String get losLegendTerrain;
|
||||||
|
|
||||||
|
/// No description provided for @losBlockedSpotsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Blocked spots'**
|
||||||
|
String get losBlockedSpotsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @losBlockedSpotsHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Tap a blocked spot to highlight it on the map.'**
|
||||||
|
String get losBlockedSpotsHint;
|
||||||
|
|
||||||
|
/// No description provided for @losBlockedSpotChip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{distance} {distanceUnit} • {obstruction} {heightUnit}'**
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// No description provided for @losSelectedObstructionTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Selected obstruction'**
|
||||||
|
String get losSelectedObstructionTitle;
|
||||||
|
|
||||||
|
/// No description provided for @losSelectedObstructionDetails.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).'**
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
);
|
||||||
|
|
||||||
/// No description provided for @losFrequencyLabel.
|
/// No description provided for @losFrequencyLabel.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -1576,6 +1576,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
return 'Непрочетени: $count';
|
return 'Непрочетени: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Отваряне на връзката?';
|
String get chat_openLink => 'Отваряне на връзката?';
|
||||||
|
|
||||||
@@ -1760,6 +1766,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Споделено копие';
|
String get map_sharedPin => 'Споделено копие';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Споделено';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Присъедини се към стаята';
|
String get map_joinRoom => 'Присъедини се към стаята';
|
||||||
|
|
||||||
@@ -3272,6 +3281,37 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Терен';
|
String get losLegendTerrain => 'Терен';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Ограничени места';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Кликнете върху блокираната точка, за да я отбележите на картата.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Избрано препятствие';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Честота';
|
String get losFrequencyLabel => 'Честота';
|
||||||
|
|
||||||
|
|||||||
@@ -1573,6 +1573,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
return 'Ungelesen: $count';
|
return 'Ungelesen: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Link öffnen?';
|
String get chat_openLink => 'Link öffnen?';
|
||||||
|
|
||||||
@@ -1757,6 +1763,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Gemeinsames Passwort';
|
String get map_sharedPin => 'Gemeinsames Passwort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Geteilt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Beitreten Sie dem Raum';
|
String get map_joinRoom => 'Beitreten Sie dem Raum';
|
||||||
|
|
||||||
@@ -3277,6 +3286,37 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Gelände';
|
String get losLegendTerrain => 'Gelände';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Reservierte Plätze';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance • $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Ausgewählte Behinderung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blockiert durch $obstruction in einer Höhe von $heightUnit, $distanceFromA von A und $distanceFromB von B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Frequenz';
|
String get losFrequencyLabel => 'Frequenz';
|
||||||
|
|
||||||
|
|||||||
@@ -1543,6 +1543,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'Unread: $count';
|
return 'Unread: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Open Link?';
|
String get chat_openLink => 'Open Link?';
|
||||||
|
|
||||||
@@ -1726,6 +1732,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Shared pin';
|
String get map_sharedPin => 'Shared pin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Shared';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Join Room';
|
String get map_joinRoom => 'Join Room';
|
||||||
|
|
||||||
@@ -3216,6 +3225,37 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Terrain';
|
String get losLegendTerrain => 'Terrain';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Blocked spots';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Tap a blocked spot to highlight it on the map.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Selected obstruction';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Frequency';
|
String get losFrequencyLabel => 'Frequency';
|
||||||
|
|
||||||
|
|||||||
@@ -1572,6 +1572,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
return 'Sin leer: $count';
|
return 'Sin leer: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => '¿Abrir enlace?';
|
String get chat_openLink => '¿Abrir enlace?';
|
||||||
|
|
||||||
@@ -1756,6 +1762,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Pin compartido';
|
String get map_sharedPin => 'Pin compartido';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Compartido';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Únete a la sala';
|
String get map_joinRoom => 'Únete a la sala';
|
||||||
|
|
||||||
@@ -3271,6 +3280,37 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Terreno';
|
String get losLegendTerrain => 'Terreno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Espacios ocupados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Seleccione un punto bloqueado para resaltarlo en el mapa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Obstrucción seleccionada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Bloqueado por $obstruction a una altura de $heightUnit, a $distanceFromA metros de A y a $distanceFromB metros de B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Frecuencia';
|
String get losFrequencyLabel => 'Frecuencia';
|
||||||
|
|
||||||
|
|||||||
@@ -1581,6 +1581,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
return 'Non lu : $count';
|
return 'Non lu : $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Ouvrir le lien ?';
|
String get chat_openLink => 'Ouvrir le lien ?';
|
||||||
|
|
||||||
@@ -1766,6 +1772,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Clé partagée';
|
String get map_sharedPin => 'Clé partagée';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Partagé';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Rejoindre le room server';
|
String get map_joinRoom => 'Rejoindre le room server';
|
||||||
|
|
||||||
@@ -3289,6 +3298,37 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Terrain';
|
String get losLegendTerrain => 'Terrain';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Places occupés';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Obstruction sélectionnée';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Bloqué par $obstruction, à une hauteur de $heightUnit, à une distance de $distanceFromA par rapport à A et à une distance de $distanceFromB par rapport à B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Fréquence';
|
String get losFrequencyLabel => 'Fréquence';
|
||||||
|
|
||||||
|
|||||||
@@ -1583,6 +1583,12 @@ class AppLocalizationsHu extends AppLocalizations {
|
|||||||
return 'Olvasatlan: $count';
|
return 'Olvasatlan: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Nyisd meg a linket?';
|
String get chat_openLink => 'Nyisd meg a linket?';
|
||||||
|
|
||||||
@@ -1769,6 +1775,9 @@ class AppLocalizationsHu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Gemeinsames PIN-kód';
|
String get map_sharedPin => 'Gemeinsames PIN-kód';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Megosztva';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Csatlakozás a szobához';
|
String get map_joinRoom => 'Csatlakozás a szobához';
|
||||||
|
|
||||||
@@ -3285,6 +3294,37 @@ class AppLocalizationsHu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Terület';
|
String get losLegendTerrain => 'Terület';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Foglalhatatlan területek';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'A blokkolt területet megjelölve, hogy a térképen kiemeljük.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Kiválasztott akadály';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Elakadt a $obstruction miatt, $heightUnit magasságban, $distanceFromA méterrel A-tól és $distanceFromB méterrel B-től ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Hatósság';
|
String get losFrequencyLabel => 'Hatósság';
|
||||||
|
|
||||||
|
|||||||
@@ -1574,6 +1574,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
return 'Non letti: $count';
|
return 'Non letti: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Aprire il link?';
|
String get chat_openLink => 'Aprire il link?';
|
||||||
|
|
||||||
@@ -1757,6 +1763,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Condividi PIN';
|
String get map_sharedPin => 'Condividi PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Condiviso';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Unisciti alla stanza';
|
String get map_joinRoom => 'Unisciti alla stanza';
|
||||||
|
|
||||||
@@ -3274,6 +3283,37 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Terreno';
|
String get losLegendTerrain => 'Terreno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Posti occupati';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Tocca un punto bloccato sulla mappa per evidenziarlo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Ostacolo selezionato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Frequenza';
|
String get losFrequencyLabel => 'Frequenza';
|
||||||
|
|
||||||
|
|||||||
@@ -1505,6 +1505,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
return '未読: $count';
|
return '未読: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'リンクを開く?';
|
String get chat_openLink => 'リンクを開く?';
|
||||||
|
|
||||||
@@ -1684,6 +1690,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => '共有パスワード';
|
String get map_sharedPin => '共有パスワード';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => '共有済み';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => '部屋に参加する';
|
String get map_joinRoom => '部屋に参加する';
|
||||||
|
|
||||||
@@ -3126,6 +3135,36 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => '地形';
|
String get losLegendTerrain => '地形';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => '利用できない場所';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint => '地図上で、特定された場所を強調するために、該当する場所をタップしてください。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => '選択された障害';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return '$obstruction によって $heightUnit の高さで、A地点から $distanceFromA、B地点から $distanceFromB ($distanceUnit) の距離で塞がれています。';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => '周波数';
|
String get losFrequencyLabel => '周波数';
|
||||||
|
|
||||||
|
|||||||
@@ -1501,6 +1501,12 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||||||
return '읽지 않음: $count';
|
return '읽지 않음: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => '링크를 열기?';
|
String get chat_openLink => '링크를 열기?';
|
||||||
|
|
||||||
@@ -1680,6 +1686,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => '공유 비밀번호';
|
String get map_sharedPin => '공유 비밀번호';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => '공유됨';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => '방에 참여';
|
String get map_joinRoom => '방에 참여';
|
||||||
|
|
||||||
@@ -3126,6 +3135,36 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => '지형';
|
String get losLegendTerrain => '지형';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => '차단된 공간';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint => '지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => '선택된 장애물';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => '빈도';
|
String get losFrequencyLabel => '빈도';
|
||||||
|
|
||||||
|
|||||||
@@ -1561,6 +1561,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
return 'Nieuw: $count';
|
return 'Nieuw: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Link openen?';
|
String get chat_openLink => 'Link openen?';
|
||||||
|
|
||||||
@@ -1745,6 +1751,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Gedeelde pin';
|
String get map_sharedPin => 'Gedeelde pin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Gedeeld';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Kamer Toetreden';
|
String get map_joinRoom => 'Kamer Toetreden';
|
||||||
|
|
||||||
@@ -3256,6 +3265,37 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Terrein';
|
String get losLegendTerrain => 'Terrein';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Geplande plaatsen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Tik op een geblokkeerd gebied om het op de kaart te markeren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Geselecteerde obstakel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Frequentie';
|
String get losFrequencyLabel => 'Frequentie';
|
||||||
|
|
||||||
|
|||||||
@@ -1585,6 +1585,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
return 'Nieprzeczytane: $count';
|
return 'Nieprzeczytane: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Otworzyć link?';
|
String get chat_openLink => 'Otworzyć link?';
|
||||||
|
|
||||||
@@ -1769,6 +1775,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Udostępniona pinezka';
|
String get map_sharedPin => 'Udostępniona pinezka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Udostępnione';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Dołącz do pokoju';
|
String get map_joinRoom => 'Dołącz do pokoju';
|
||||||
|
|
||||||
@@ -3281,6 +3290,37 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Teren';
|
String get losLegendTerrain => 'Teren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Zablokowane miejsca';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Wybór przeszkody';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Częstotliwość';
|
String get losFrequencyLabel => 'Częstotliwość';
|
||||||
|
|
||||||
|
|||||||
@@ -1572,6 +1572,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
return 'Não lido: $count';
|
return 'Não lido: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Abrir link?';
|
String get chat_openLink => 'Abrir link?';
|
||||||
|
|
||||||
@@ -1757,6 +1763,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Pin compartilhado';
|
String get map_sharedPin => 'Pin compartilhado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Compartilhado';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Junte-se à Sala';
|
String get map_joinRoom => 'Junte-se à Sala';
|
||||||
|
|
||||||
@@ -3270,6 +3279,37 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Terreno';
|
String get losLegendTerrain => 'Terreno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Locais ocupados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Toque em um ponto bloqueado para destacá-lo no mapa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Obstrução selecionada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Frequência';
|
String get losFrequencyLabel => 'Frequência';
|
||||||
|
|
||||||
|
|||||||
@@ -1576,6 +1576,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Непрочитанных: $count';
|
return 'Непрочитанных: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Открыть ссылку?';
|
String get chat_openLink => 'Открыть ссылку?';
|
||||||
|
|
||||||
@@ -1760,6 +1766,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Общая метка';
|
String get map_sharedPin => 'Общая метка';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Поделено';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Присоединиться к комнате';
|
String get map_joinRoom => 'Присоединиться к комнате';
|
||||||
|
|
||||||
@@ -3275,6 +3284,38 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Рельеф';
|
String get losLegendTerrain => 'Рельеф';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Зарезервированные места';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Щелкните по заблокированной области, чтобы выделить ее на карте.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle =>
|
||||||
|
'Выбранный объект, препятствующий движению';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Частота';
|
String get losFrequencyLabel => 'Частота';
|
||||||
|
|
||||||
|
|||||||
@@ -1562,6 +1562,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
return 'Nezriadené: $count';
|
return 'Nezriadené: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Otvoriť odkaz?';
|
String get chat_openLink => 'Otvoriť odkaz?';
|
||||||
|
|
||||||
@@ -1746,6 +1752,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Zdieľaný PIN';
|
String get map_sharedPin => 'Zdieľaný PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Zdieľané';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Pripojiť miestnosť';
|
String get map_joinRoom => 'Pripojiť miestnosť';
|
||||||
|
|
||||||
@@ -3250,6 +3259,37 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Terén';
|
String get losLegendTerrain => 'Terén';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Zablokované miesta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Vybraná prekážka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Frekvencia';
|
String get losFrequencyLabel => 'Frekvencia';
|
||||||
|
|
||||||
|
|||||||
@@ -1557,6 +1557,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
return 'Nerešeno: $count';
|
return 'Nerešeno: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Odpreti povezavo?';
|
String get chat_openLink => 'Odpreti povezavo?';
|
||||||
|
|
||||||
@@ -1741,6 +1747,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Deljeno naslovno geslo';
|
String get map_sharedPin => 'Deljeno naslovno geslo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Deljeno';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Pridružiti sobo';
|
String get map_joinRoom => 'Pridružiti sobo';
|
||||||
|
|
||||||
@@ -3251,6 +3260,37 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Teren';
|
String get losLegendTerrain => 'Teren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Zasedena parkirišča';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Dotaknite blokirano točko, da jo označite na zemljeplati.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Izbrano ovire';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Frekvenca';
|
String get losFrequencyLabel => 'Frekvenca';
|
||||||
|
|
||||||
|
|||||||
@@ -1551,6 +1551,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
return 'Olästa: $count';
|
return 'Olästa: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Öppna länk?';
|
String get chat_openLink => 'Öppna länk?';
|
||||||
|
|
||||||
@@ -1735,6 +1741,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Delad PIN';
|
String get map_sharedPin => 'Delad PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Delad';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Gå med i rum';
|
String get map_joinRoom => 'Gå med i rum';
|
||||||
|
|
||||||
@@ -3233,6 +3242,37 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Terräng';
|
String get losLegendTerrain => 'Terräng';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Reserverade platser';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Klicka på en markerad plats för att framhäva den på kartan.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Vald hinder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Frekvens';
|
String get losFrequencyLabel => 'Frekvens';
|
||||||
|
|
||||||
|
|||||||
@@ -1571,6 +1571,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
return 'Непрочитано: $count';
|
return 'Непрочитано: $count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => 'Відкрити посилання?';
|
String get chat_openLink => 'Відкрити посилання?';
|
||||||
|
|
||||||
@@ -1755,6 +1761,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => 'Спільний пін';
|
String get map_sharedPin => 'Спільний пін';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => 'Поділено';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => 'Приєднатись до кімнати';
|
String get map_joinRoom => 'Приєднатись до кімнати';
|
||||||
|
|
||||||
@@ -3278,6 +3287,37 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => 'Рельєф';
|
String get losLegendTerrain => 'Рельєф';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => 'Заблоковані місця';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint =>
|
||||||
|
'Натисніть на заблоковане місце, щоб виділити його на карті.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => 'Вибраний об\'єкт перешкоди';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => 'Частота';
|
String get losFrequencyLabel => 'Частота';
|
||||||
|
|
||||||
|
|||||||
@@ -1473,6 +1473,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
return '未读:$count';
|
return '未读:$count';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_markAsUnread => 'Mark as Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_newMessages => 'New messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_openLink => '打开链接?';
|
String get chat_openLink => '打开链接?';
|
||||||
|
|
||||||
@@ -1652,6 +1658,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_sharedPin => '共享标记';
|
String get map_sharedPin => '共享标记';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_sharedAt => '已分享';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_joinRoom => '加入房间';
|
String get map_joinRoom => '加入房间';
|
||||||
|
|
||||||
@@ -3050,6 +3059,36 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get losLegendTerrain => '地形';
|
String get losLegendTerrain => '地形';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsTitle => '被占用区域';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losBlockedSpotsHint => '点击地图上的某个被遮盖的区域,以突出显示该区域。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losBlockedSpotChip(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectedObstructionTitle => '选择性阻碍';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losSelectedObstructionDetails(
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
String distanceFromA,
|
||||||
|
String distanceUnit,
|
||||||
|
String distanceFromB,
|
||||||
|
) {
|
||||||
|
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get losFrequencyLabel => '频率';
|
String get losFrequencyLabel => '频率';
|
||||||
|
|
||||||
|
|||||||
+42
-1
@@ -2108,5 +2108,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "Meerdere bevestigingen"
|
"settings_multiAck": "Meerdere bevestigingen",
|
||||||
|
"map_sharedAt": "Gedeeld",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losSelectedObstructionTitle": "Geselecteerde obstakel",
|
||||||
|
"losBlockedSpotsHint": "Tik op een geblokkeerd gebied om het op de kaart te markeren.",
|
||||||
|
"losBlockedSpotsTitle": "Geplande plaatsen",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2146,5 +2146,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "Wielokrotne potwierdzenia odbioru"
|
"settings_multiAck": "Wielokrotne potwierdzenia odbioru",
|
||||||
|
"map_sharedAt": "Udostępnione",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losSelectedObstructionTitle": "Wybór przeszkody",
|
||||||
|
"losBlockedSpotsTitle": "Zablokowane miejsca",
|
||||||
|
"losBlockedSpotsHint": "Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2108,5 +2108,46 @@
|
|||||||
"contact_typeSensor": "Sensor",
|
"contact_typeSensor": "Sensor",
|
||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score"
|
"chat_score": "Score",
|
||||||
|
"map_sharedAt": "Compartilhado",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsTitle": "Locais ocupados",
|
||||||
|
"losBlockedSpotsHint": "Toque em um ponto bloqueado para destacá-lo no mapa.",
|
||||||
|
"losSelectedObstructionTitle": "Obstrução selecionada",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -1348,5 +1348,46 @@
|
|||||||
"contact_typeUnknown": "Неизвестно",
|
"contact_typeUnknown": "Неизвестно",
|
||||||
"channels_via": "через {path}",
|
"channels_via": "через {path}",
|
||||||
"chat_score": "Оценка",
|
"chat_score": "Оценка",
|
||||||
"settings_multiAck": "Несколько подтверждений"
|
"settings_multiAck": "Несколько подтверждений",
|
||||||
|
"map_sharedAt": "Поделено",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsHint": "Щелкните по заблокированной области, чтобы выделить ее на карте.",
|
||||||
|
"losBlockedSpotsTitle": "Зарезервированные места",
|
||||||
|
"losSelectedObstructionTitle": "Выбранный объект, препятствующий движению",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2108,5 +2108,46 @@
|
|||||||
"contact_typeSensor": "Sensor",
|
"contact_typeSensor": "Sensor",
|
||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score"
|
"chat_score": "Score",
|
||||||
|
"map_sharedAt": "Zdieľané",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsTitle": "Zablokované miesta",
|
||||||
|
"losBlockedSpotsHint": "Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.",
|
||||||
|
"losSelectedObstructionTitle": "Vybraná prekážka",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2108,5 +2108,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "Več potrdil"
|
"settings_multiAck": "Več potrdil",
|
||||||
|
"map_sharedAt": "Deljeno",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsHint": "Dotaknite blokirano točko, da jo označite na zemljeplati.",
|
||||||
|
"losSelectedObstructionTitle": "Izbrano ovire",
|
||||||
|
"losBlockedSpotsTitle": "Zasedena parkirišča",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2108,5 +2108,46 @@
|
|||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score",
|
"chat_score": "Score",
|
||||||
"settings_multiAck": "Flera bekräftelser"
|
"settings_multiAck": "Flera bekräftelser",
|
||||||
|
"map_sharedAt": "Delad",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losBlockedSpotsTitle": "Reserverade platser",
|
||||||
|
"losSelectedObstructionTitle": "Vald hinder",
|
||||||
|
"losBlockedSpotsHint": "Klicka på en markerad plats för att framhäva den på kartan.",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2087,5 +2087,46 @@
|
|||||||
"contact_typeRoom": "Кімната",
|
"contact_typeRoom": "Кімната",
|
||||||
"contact_typeSensor": "Сенсор",
|
"contact_typeSensor": "Сенсор",
|
||||||
"contact_typeUnknown": "Невідомо",
|
"contact_typeUnknown": "Невідомо",
|
||||||
"settings_multiAck": "Багато підтверджень"
|
"settings_multiAck": "Багато підтверджень",
|
||||||
|
"map_sharedAt": "Поділено",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsHint": "Натисніть на заблоковане місце, щоб виділити його на карті.",
|
||||||
|
"losBlockedSpotsTitle": "Заблоковані місця",
|
||||||
|
"losSelectedObstructionTitle": "Вибраний об'єкт перешкоди",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -2113,5 +2113,46 @@
|
|||||||
"contact_typeSensor": "Sensor",
|
"contact_typeSensor": "Sensor",
|
||||||
"contact_typeUnknown": "Unknown",
|
"contact_typeUnknown": "Unknown",
|
||||||
"channels_via": "via {path}",
|
"channels_via": "via {path}",
|
||||||
"chat_score": "Score"
|
"chat_score": "Score",
|
||||||
|
"map_sharedAt": "已分享",
|
||||||
|
"@losBlockedSpotChip": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@losSelectedObstructionDetails": {
|
||||||
|
"placeholders": {
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromA": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceFromB": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losBlockedSpotsTitle": "被占用区域",
|
||||||
|
"losBlockedSpotsHint": "点击地图上的某个被遮盖的区域,以突出显示该区域。",
|
||||||
|
"losSelectedObstructionTitle": "选择性阻碍",
|
||||||
|
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||||
|
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
@@ -33,13 +32,19 @@ import '../widgets/message_translation_button.dart';
|
|||||||
import '../widgets/message_status_icon.dart';
|
import '../widgets/message_status_icon.dart';
|
||||||
import '../widgets/radio_stats_entry.dart';
|
import '../widgets/radio_stats_entry.dart';
|
||||||
import '../widgets/translated_message_content.dart';
|
import '../widgets/translated_message_content.dart';
|
||||||
|
import '../widgets/unread_divider.dart';
|
||||||
import 'channel_message_path_screen.dart';
|
import 'channel_message_path_screen.dart';
|
||||||
import 'map_screen.dart';
|
import 'map_screen.dart';
|
||||||
|
|
||||||
class ChannelChatScreen extends StatefulWidget {
|
class ChannelChatScreen extends StatefulWidget {
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
|
final int initialUnreadCount;
|
||||||
|
|
||||||
const ChannelChatScreen({super.key, required this.channel});
|
const ChannelChatScreen({
|
||||||
|
super.key,
|
||||||
|
required this.channel,
|
||||||
|
this.initialUnreadCount = 0,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
||||||
@@ -56,6 +61,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
MeshCoreConnector? _connector;
|
MeshCoreConnector? _connector;
|
||||||
DateTime? _lastChannelSendAt;
|
DateTime? _lastChannelSendAt;
|
||||||
bool _channelSkipNextBottomSnap = false;
|
bool _channelSkipNextBottomSnap = false;
|
||||||
|
String? _unreadDividerMessageId;
|
||||||
|
|
||||||
String? _cachedFormatLocale;
|
String? _cachedFormatLocale;
|
||||||
late DateFormat _hmFormat;
|
late DateFormat _hmFormat;
|
||||||
@@ -66,26 +72,35 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
|
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
|
||||||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||||
|
_scrollController.showJumpToBottom.addListener(_clearDividerAtBottom);
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final connector = context.read<MeshCoreConnector>();
|
final connector = context.read<MeshCoreConnector>();
|
||||||
final settings = context.read<AppSettingsService>().settings;
|
final settings = context.read<AppSettingsService>().settings;
|
||||||
final idx = widget.channel.index;
|
final idx = widget.channel.index;
|
||||||
final unread = connector.getUnreadCountForChannelIndex(idx);
|
final unread = widget.initialUnreadCount;
|
||||||
|
final messages = connector.getChannelMessages(widget.channel);
|
||||||
ChannelMessage? anchor;
|
ChannelMessage? anchor;
|
||||||
if (settings.jumpToOldestUnread && unread > 0) {
|
if (unread > 0) {
|
||||||
anchor = _findOldestUnreadChannelAnchor(
|
anchor = _findOldestUnreadChannelAnchor(messages, unread);
|
||||||
connector.getChannelMessages(widget.channel),
|
|
||||||
unread,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
setState(() {
|
||||||
|
if (anchor != null) _unreadDividerMessageId = anchor.messageId;
|
||||||
|
});
|
||||||
connector.setActiveChannel(idx);
|
connector.setActiveChannel(idx);
|
||||||
_connector = connector;
|
_connector = connector;
|
||||||
if (anchor != null) {
|
if (anchor != null && settings.jumpToOldestUnread) {
|
||||||
_channelSkipNextBottomSnap = true;
|
_channelSkipNextBottomSnap = true;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_scrollToMessage(anchor!.messageId);
|
_scrollController.jumpToEstimatedOffset(
|
||||||
|
unreadCount: unread,
|
||||||
|
totalMessages: messages.length,
|
||||||
|
onJumped: () {
|
||||||
|
if (!mounted) return;
|
||||||
|
_scrollToMessage(anchor!.messageId);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -107,6 +122,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
return oldest;
|
return oldest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearDividerAtBottom() {
|
||||||
|
if (!_scrollController.showJumpToBottom.value &&
|
||||||
|
_unreadDividerMessageId != null) {
|
||||||
|
setState(() => _unreadDividerMessageId = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onTextFieldFocusChange() {
|
void _onTextFieldFocusChange() {
|
||||||
if (_textFieldFocusNode.hasFocus && mounted) {
|
if (_textFieldFocusNode.hasFocus && mounted) {
|
||||||
_scrollController.handleKeyboardOpen();
|
_scrollController.handleKeyboardOpen();
|
||||||
@@ -128,6 +150,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_connector?.setActiveChannel(null);
|
_connector?.setActiveChannel(null);
|
||||||
|
_scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom);
|
||||||
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
||||||
_textFieldFocusNode.dispose();
|
_textFieldFocusNode.dispose();
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
@@ -326,6 +349,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
if (!_messageKeys.containsKey(message.messageId)) {
|
if (!_messageKeys.containsKey(message.messageId)) {
|
||||||
_messageKeys[message.messageId] = GlobalKey();
|
_messageKeys[message.messageId] = GlobalKey();
|
||||||
}
|
}
|
||||||
|
final isUnreadAnchor =
|
||||||
|
_unreadDividerMessageId != null &&
|
||||||
|
message.messageId == _unreadDividerMessageId;
|
||||||
return Container(
|
return Container(
|
||||||
key: _messageKeys[message.messageId]!,
|
key: _messageKeys[message.messageId]!,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
@@ -334,10 +360,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
.select<ChatTextScaleService, double>(
|
.select<ChatTextScaleService, double>(
|
||||||
(service) => service.scale,
|
(service) => service.scale,
|
||||||
);
|
);
|
||||||
return _buildMessageBubble(
|
final bubble = _buildMessageBubble(
|
||||||
message,
|
message,
|
||||||
textScale,
|
textScale,
|
||||||
);
|
);
|
||||||
|
if (isUnreadAnchor) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [const UnreadDivider(), bubble],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return bubble;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -357,12 +390,24 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _markAsUnread(ChannelMessage message) {
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
final messages = connector.getChannelMessages(widget.channel);
|
||||||
|
var count = 0;
|
||||||
|
var found = false;
|
||||||
|
for (final m in messages) {
|
||||||
|
if (m.messageId == message.messageId) found = true;
|
||||||
|
if (found && !m.isOutgoing) count++;
|
||||||
|
}
|
||||||
|
connector.setChannelUnreadCount(widget.channel.index, count);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildMessageBubble(ChannelMessage message, double textScale) {
|
Widget _buildMessageBubble(ChannelMessage message, double textScale) {
|
||||||
final settingsService = context.watch<AppSettingsService>();
|
final settingsService = context.watch<AppSettingsService>();
|
||||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||||
final isOutgoing = message.isOutgoing;
|
final isOutgoing = message.isOutgoing;
|
||||||
final gifId = GifHelper.parseGif(message.text);
|
final gifId = GifHelper.parseGif(message.text);
|
||||||
final poi = _parsePoiMessage(message.text);
|
final poi = parseMarkerText(message.text);
|
||||||
final translatedDisplayText =
|
final translatedDisplayText =
|
||||||
message.translatedText != null &&
|
message.translatedText != null &&
|
||||||
message.translatedText!.trim().isNotEmpty
|
message.translatedText!.trim().isNotEmpty
|
||||||
@@ -450,6 +495,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
poi,
|
poi,
|
||||||
isOutgoing,
|
isOutgoing,
|
||||||
textScale,
|
textScale,
|
||||||
|
message.senderName,
|
||||||
trailing: (!enableTracing && isOutgoing)
|
trailing: (!enableTracing && isOutgoing)
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
@@ -708,7 +754,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
|
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
|
||||||
|
|
||||||
final gifId = GifHelper.parseGif(replyText);
|
final gifId = GifHelper.parseGif(replyText);
|
||||||
final poi = _parsePoiMessage(replyText);
|
final poi = parseMarkerText(replyText);
|
||||||
|
|
||||||
Widget contentPreview;
|
Widget contentPreview;
|
||||||
if (gifId != null) {
|
if (gifId != null) {
|
||||||
@@ -819,24 +865,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_PoiInfo? _parsePoiMessage(String text) {
|
|
||||||
final trimmed = text.trim();
|
|
||||||
final match = RegExp(
|
|
||||||
r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|',
|
|
||||||
).firstMatch(trimmed);
|
|
||||||
if (match == null) return null;
|
|
||||||
final lat = double.tryParse(match.group(1) ?? '');
|
|
||||||
final lon = double.tryParse(match.group(2) ?? '');
|
|
||||||
if (lat == null || lon == null) return null;
|
|
||||||
final label = match.group(3) ?? '';
|
|
||||||
return _PoiInfo(lat: lat, lon: lon, label: label);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPoiMessage(
|
Widget _buildPoiMessage(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
_PoiInfo poi,
|
MarkerPayload poi,
|
||||||
bool isOutgoing,
|
bool isOutgoing,
|
||||||
double textScale, {
|
double textScale,
|
||||||
|
String senderName, {
|
||||||
Widget? trailing,
|
Widget? trailing,
|
||||||
}) {
|
}) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
@@ -856,12 +890,22 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
final selfName = context.read<MeshCoreConnector>().selfName ?? 'Me';
|
||||||
|
final fromName = isOutgoing ? selfName : senderName;
|
||||||
|
final key = buildSharedMarkerKey(
|
||||||
|
sourceId: 'channel:${widget.channel.index}',
|
||||||
|
label: poi.label,
|
||||||
|
fromName: fromName,
|
||||||
|
flags: poi.flags,
|
||||||
|
isChannel: true,
|
||||||
|
);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => MapScreen(
|
builder: (context) => MapScreen(
|
||||||
highlightPosition: LatLng(poi.lat, poi.lon),
|
highlightPosition: poi.position,
|
||||||
highlightLabel: poi.label,
|
highlightLabel: poi.label,
|
||||||
|
highlightMarkerKey: key,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1302,6 +1346,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
_copyMessageText(message.text);
|
_copyMessageText(message.text);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (!message.isOutgoing)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||||
|
title: Text(context.l10n.chat_markAsUnread),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_markAsUnread(message);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_outline),
|
leading: const Icon(Icons.delete_outline),
|
||||||
title: Text(context.l10n.common_delete),
|
title: Text(context.l10n.common_delete),
|
||||||
@@ -1521,11 +1574,3 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PoiInfo {
|
|
||||||
final double lat;
|
|
||||||
final double lon;
|
|
||||||
final String label;
|
|
||||||
|
|
||||||
const _PoiInfo({required this.lat, required this.lon, required this.label});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -492,13 +492,18 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
final unread =
|
||||||
|
connector.getUnreadCountForChannelIndex(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) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ChannelChatScreen(channel: channel),
|
builder: (context) => ChannelChatScreen(
|
||||||
|
channel: channel,
|
||||||
|
initialUnreadCount: unread,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+105
-50
@@ -9,7 +9,6 @@ import 'package:meshcore_open/screens/path_trace_map.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../utils/platform_info.dart';
|
import '../utils/platform_info.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
@@ -45,12 +44,18 @@ import '../widgets/translated_message_content.dart';
|
|||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../helpers/snack_bar_builder.dart';
|
import '../helpers/snack_bar_builder.dart';
|
||||||
|
import '../widgets/unread_divider.dart';
|
||||||
import 'telemetry_screen.dart';
|
import 'telemetry_screen.dart';
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatefulWidget {
|
||||||
final Contact contact;
|
final Contact contact;
|
||||||
|
final int initialUnreadCount;
|
||||||
|
|
||||||
const ChatScreen({super.key, required this.contact});
|
const ChatScreen({
|
||||||
|
super.key,
|
||||||
|
required this.contact,
|
||||||
|
this.initialUnreadCount = 0,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatScreen> createState() => _ChatScreenState();
|
State<ChatScreen> createState() => _ChatScreenState();
|
||||||
@@ -64,6 +69,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
bool _isLoadingOlder = false;
|
bool _isLoadingOlder = false;
|
||||||
MeshCoreConnector? _connector;
|
MeshCoreConnector? _connector;
|
||||||
Message? _pendingUnreadScrollTarget;
|
Message? _pendingUnreadScrollTarget;
|
||||||
|
String? _unreadDividerMessageId;
|
||||||
DateTime? _lastTextSendAt;
|
DateTime? _lastTextSendAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -71,34 +77,47 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
|
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
|
||||||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||||
|
_scrollController.showJumpToBottom.addListener(_clearDividerAtBottom);
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final connector = context.read<MeshCoreConnector>();
|
final connector = context.read<MeshCoreConnector>();
|
||||||
final settings = context.read<AppSettingsService>().settings;
|
final settings = context.read<AppSettingsService>().settings;
|
||||||
final keyHex = widget.contact.publicKeyHex;
|
final keyHex = widget.contact.publicKeyHex;
|
||||||
final unread = connector.getUnreadCountForContactKey(keyHex);
|
final unread = widget.initialUnreadCount;
|
||||||
|
final messages = connector.getMessages(widget.contact);
|
||||||
Message? anchor;
|
Message? anchor;
|
||||||
if (settings.jumpToOldestUnread && unread > 0) {
|
if (unread > 0) {
|
||||||
anchor = _findOldestUnreadAnchor(
|
anchor = _findOldestUnreadAnchor(messages, unread);
|
||||||
connector.getMessages(widget.contact),
|
|
||||||
unread,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
setState(() {
|
||||||
|
if (anchor != null) _unreadDividerMessageId = anchor.messageId;
|
||||||
|
if (anchor != null && settings.jumpToOldestUnread) {
|
||||||
|
_pendingUnreadScrollTarget = anchor;
|
||||||
|
}
|
||||||
|
});
|
||||||
connector.setActiveContact(keyHex);
|
connector.setActiveContact(keyHex);
|
||||||
_connector = connector;
|
_connector = connector;
|
||||||
if (anchor != null) {
|
if (anchor != null && settings.jumpToOldestUnread) {
|
||||||
setState(() => _pendingUnreadScrollTarget = anchor);
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final ctx = _unreadScrollKey.currentContext;
|
_scrollController.jumpToEstimatedOffset(
|
||||||
if (ctx != null) {
|
unreadCount: unread,
|
||||||
Scrollable.ensureVisible(
|
totalMessages: messages.length,
|
||||||
ctx,
|
onJumped: () async {
|
||||||
duration: const Duration(milliseconds: 350),
|
if (!mounted) return;
|
||||||
alignment: 0.15,
|
final ctx = _unreadScrollKey.currentContext;
|
||||||
);
|
if (ctx != null) {
|
||||||
}
|
await Scrollable.ensureVisible(
|
||||||
setState(() => _pendingUnreadScrollTarget = null);
|
ctx,
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
alignment: 0.15,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _pendingUnreadScrollTarget = null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -117,6 +136,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
return oldest;
|
return oldest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearDividerAtBottom() {
|
||||||
|
if (!_scrollController.showJumpToBottom.value &&
|
||||||
|
_unreadDividerMessageId != null) {
|
||||||
|
setState(() => _unreadDividerMessageId = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onTextFieldFocusChange() {
|
void _onTextFieldFocusChange() {
|
||||||
if (_textFieldFocusNode.hasFocus && mounted) {
|
if (_textFieldFocusNode.hasFocus && mounted) {
|
||||||
_scrollController.handleKeyboardOpen();
|
_scrollController.handleKeyboardOpen();
|
||||||
@@ -138,6 +164,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_connector?.setActiveContact(null);
|
_connector?.setActiveContact(null);
|
||||||
|
_scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom);
|
||||||
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
||||||
_textFieldFocusNode.dispose();
|
_textFieldFocusNode.dispose();
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
@@ -480,6 +507,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
senderName: resolvedContact.type == advTypeRoom
|
senderName: resolvedContact.type == advTypeRoom
|
||||||
? "${contact.name} [$fourByteHex]"
|
? "${contact.name} [$fourByteHex]"
|
||||||
: contact.name,
|
: contact.name,
|
||||||
|
sourceId: widget.contact.publicKeyHex,
|
||||||
isRoomServer: resolvedContact.type == advTypeRoom,
|
isRoomServer: resolvedContact.type == advTypeRoom,
|
||||||
textScale: textScale,
|
textScale: textScale,
|
||||||
onTap: () => _openMessagePath(message, contact),
|
onTap: () => _openMessagePath(message, contact),
|
||||||
@@ -487,10 +515,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
onRetryReaction: (msg, emoji) =>
|
onRetryReaction: (msg, emoji) =>
|
||||||
_sendReaction(msg, contact, emoji),
|
_sendReaction(msg, contact, emoji),
|
||||||
);
|
);
|
||||||
|
final isUnreadAnchor =
|
||||||
|
_unreadDividerMessageId != null &&
|
||||||
|
message.messageId == _unreadDividerMessageId;
|
||||||
|
final child = isUnreadAnchor
|
||||||
|
? Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [const UnreadDivider(), bubble],
|
||||||
|
)
|
||||||
|
: bubble;
|
||||||
if (identical(message, _pendingUnreadScrollTarget)) {
|
if (identical(message, _pendingUnreadScrollTarget)) {
|
||||||
return KeyedSubtree(key: _unreadScrollKey, child: bubble);
|
return KeyedSubtree(key: _unreadScrollKey, child: child);
|
||||||
}
|
}
|
||||||
return bubble;
|
return child;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -498,6 +535,18 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _markAsUnread(Message message) {
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
final messages = connector.getMessages(widget.contact);
|
||||||
|
var count = 0;
|
||||||
|
var found = false;
|
||||||
|
for (final m in messages) {
|
||||||
|
if (m.messageId == message.messageId) found = true;
|
||||||
|
if (found && !m.isOutgoing && !m.isCli) count++;
|
||||||
|
}
|
||||||
|
connector.setContactUnreadCount(widget.contact.publicKeyHex, count);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildInputBar(MeshCoreConnector connector) {
|
Widget _buildInputBar(MeshCoreConnector connector) {
|
||||||
final maxBytes = maxContactMessageBytes();
|
final maxBytes = maxContactMessageBytes();
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
@@ -1321,11 +1370,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _openChat(BuildContext context, Contact contact) {
|
void _openChat(BuildContext context, Contact contact) {
|
||||||
// Check if this is a repeater
|
final connector = context.read<MeshCoreConnector>();
|
||||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
final unread = connector.getUnreadCountForContactKey(contact.publicKeyHex);
|
||||||
|
connector.markContactRead(contact.publicKeyHex);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)),
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
ChatScreen(contact: contact, initialUnreadCount: unread),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1462,6 +1515,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_copyMessageText(message.text);
|
_copyMessageText(message.text);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (!message.isOutgoing)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||||
|
title: Text(context.l10n.chat_markAsUnread),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_markAsUnread(message);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_outline),
|
leading: const Icon(Icons.delete_outline),
|
||||||
title: Text(context.l10n.common_delete),
|
title: Text(context.l10n.common_delete),
|
||||||
@@ -1569,10 +1631,12 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
final VoidCallback? onLongPress;
|
final VoidCallback? onLongPress;
|
||||||
final void Function(Message message, String emoji)? onRetryReaction;
|
final void Function(Message message, String emoji)? onRetryReaction;
|
||||||
final double textScale;
|
final double textScale;
|
||||||
|
final String sourceId;
|
||||||
|
|
||||||
const _MessageBubble({
|
const _MessageBubble({
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.senderName,
|
required this.senderName,
|
||||||
|
required this.sourceId,
|
||||||
required this.isRoomServer,
|
required this.isRoomServer,
|
||||||
required this.textScale,
|
required this.textScale,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
@@ -1587,7 +1651,7 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
final isOutgoing = message.isOutgoing;
|
final isOutgoing = message.isOutgoing;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final gifId = GifHelper.parseGif(message.text);
|
final gifId = GifHelper.parseGif(message.text);
|
||||||
final poi = _parsePoiMessage(message.text);
|
final poi = parseMarkerText(message.text);
|
||||||
final isFailed = message.status == MessageStatus.failed;
|
final isFailed = message.status == MessageStatus.failed;
|
||||||
final bubbleColor = isFailed
|
final bubbleColor = isFailed
|
||||||
? colorScheme.errorContainer
|
? colorScheme.errorContainer
|
||||||
@@ -1679,6 +1743,7 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
textColor,
|
textColor,
|
||||||
metaColor,
|
metaColor,
|
||||||
textScale,
|
textScale,
|
||||||
|
senderName,
|
||||||
trailing: (!enableTracing && isOutgoing)
|
trailing: (!enableTracing && isOutgoing)
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
@@ -1860,25 +1925,13 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_PoiInfo? _parsePoiMessage(String text) {
|
|
||||||
final trimmed = text.trim();
|
|
||||||
final match = RegExp(
|
|
||||||
r'^m:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|.*$',
|
|
||||||
).firstMatch(trimmed);
|
|
||||||
if (match == null) return null;
|
|
||||||
final lat = double.tryParse(match.group(1) ?? '');
|
|
||||||
final lon = double.tryParse(match.group(2) ?? '');
|
|
||||||
if (lat == null || lon == null) return null;
|
|
||||||
final label = match.group(3) ?? '';
|
|
||||||
return _PoiInfo(lat: lat, lon: lon, label: label);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPoiMessage(
|
Widget _buildPoiMessage(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
_PoiInfo poi,
|
MarkerPayload poi,
|
||||||
Color textColor,
|
Color textColor,
|
||||||
Color metaColor,
|
Color metaColor,
|
||||||
double textScale, {
|
double textScale,
|
||||||
|
String senderName, {
|
||||||
Widget? trailing,
|
Widget? trailing,
|
||||||
}) {
|
}) {
|
||||||
return Row(
|
return Row(
|
||||||
@@ -1888,13 +1941,23 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
icon: Icon(Icons.location_on_outlined, color: textColor),
|
icon: Icon(Icons.location_on_outlined, color: textColor),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
|
final selfName = context.read<MeshCoreConnector>().selfName ?? 'Me';
|
||||||
|
final fromName = message.isOutgoing ? selfName : senderName;
|
||||||
|
final key = buildSharedMarkerKey(
|
||||||
|
sourceId: sourceId,
|
||||||
|
label: poi.label,
|
||||||
|
fromName: fromName,
|
||||||
|
flags: poi.flags,
|
||||||
|
isChannel: false,
|
||||||
|
);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => MapScreen(
|
builder: (context) => MapScreen(
|
||||||
highlightPosition: LatLng(poi.lat, poi.lon),
|
highlightPosition: poi.position,
|
||||||
highlightLabel: poi.label,
|
highlightLabel: poi.label,
|
||||||
|
highlightMarkerKey: key,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2075,11 +2138,3 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
return '$hour:$minute';
|
return '$hour:$minute';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PoiInfo {
|
|
||||||
final double lat;
|
|
||||||
final double lon;
|
|
||||||
final String label;
|
|
||||||
|
|
||||||
const _PoiInfo({required this.lat, required this.lon, required this.label});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -931,10 +931,18 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
} else if (contact.type == advTypeRoom) {
|
} else if (contact.type == advTypeRoom) {
|
||||||
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
||||||
} else {
|
} else {
|
||||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
final unread =
|
||||||
|
connector.getUnreadCountForContactKey(contact.publicKeyHex);
|
||||||
|
connector.markContactRead(contact.publicKeyHex);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)),
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ChatScreen(
|
||||||
|
contact: contact,
|
||||||
|
initialUnreadCount: unread,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -989,7 +997,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
builder: (context) => RoomLoginDialog(
|
builder: (context) => RoomLoginDialog(
|
||||||
room: room,
|
room: room,
|
||||||
onLogin: (password, isAdmin) {
|
onLogin: (password, isAdmin) {
|
||||||
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
final unread =
|
||||||
|
connector.getUnreadCountForContactKey(room.publicKeyHex);
|
||||||
|
connector.markContactRead(room.publicKeyHex);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -1000,7 +1011,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
password: password,
|
password: password,
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
)
|
)
|
||||||
: ChatScreen(contact: room),
|
: ChatScreen(
|
||||||
|
contact: room,
|
||||||
|
initialUnreadCount: unread,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
LineOfSightPathResult? _result;
|
LineOfSightPathResult? _result;
|
||||||
|
LineOfSightObstruction? _selectedObstruction;
|
||||||
LineOfSightEndpoint? _start;
|
LineOfSightEndpoint? _start;
|
||||||
LineOfSightEndpoint? _end;
|
LineOfSightEndpoint? _end;
|
||||||
final List<LineOfSightEndpoint> _customEndpoints = [];
|
final List<LineOfSightEndpoint> _customEndpoints = [];
|
||||||
@@ -111,6 +112,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
if (start == null || end == null) {
|
if (start == null || end == null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_result = null;
|
_result = null;
|
||||||
|
_selectedObstruction = null;
|
||||||
_error = _errorSelectStartEnd;
|
_error = _errorSelectStartEnd;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -142,6 +144,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_result = result;
|
_result = result;
|
||||||
|
_selectedObstruction = _defaultObstructionFor(result);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -156,6 +159,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_result = null;
|
_result = null;
|
||||||
|
_selectedObstruction = null;
|
||||||
_error = context.l10n.losRunFailed(e.toString());
|
_error = context.l10n.losRunFailed(e.toString());
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -184,6 +188,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
void _selectFromMap(LineOfSightEndpoint endpoint) {
|
void _selectFromMap(LineOfSightEndpoint endpoint) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_result = null;
|
_result = null;
|
||||||
|
_selectedObstruction = null;
|
||||||
_error = null;
|
_error = null;
|
||||||
if (_start == null || (_start != null && _end != null)) {
|
if (_start == null || (_start != null && _end != null)) {
|
||||||
_start = endpoint;
|
_start = endpoint;
|
||||||
@@ -241,6 +246,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
_start = null;
|
_start = null;
|
||||||
_end = null;
|
_end = null;
|
||||||
_result = null;
|
_result = null;
|
||||||
|
_selectedObstruction = null;
|
||||||
_error = _errorSelectStartEnd;
|
_error = _errorSelectStartEnd;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -251,6 +257,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
if (identical(_start, endpoint)) _start = null;
|
if (identical(_start, endpoint)) _start = null;
|
||||||
if (identical(_end, endpoint)) _end = null;
|
if (identical(_end, endpoint)) _end = null;
|
||||||
_result = null;
|
_result = null;
|
||||||
|
_selectedObstruction = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +384,9 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
),
|
),
|
||||||
if (_result != null && _result!.segments.isNotEmpty)
|
if (_result != null && _result!.segments.isNotEmpty)
|
||||||
PolylineLayer(polylines: _buildSegmentPolylines(_result!)),
|
PolylineLayer(polylines: _buildSegmentPolylines(_result!)),
|
||||||
MarkerLayer(markers: _buildMarkers(endpoints)),
|
MarkerLayer(
|
||||||
|
markers: _buildMarkers(endpoints, _primaryObstructions()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (_showHud)
|
if (_showHud)
|
||||||
@@ -445,6 +454,8 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
);
|
);
|
||||||
final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz;
|
final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz;
|
||||||
final kFactorUsed = segment?.usedKFactor;
|
final kFactorUsed = segment?.usedKFactor;
|
||||||
|
final obstructions =
|
||||||
|
segment?.obstructions ?? const <LineOfSightObstruction>[];
|
||||||
final endpoints = _visibleEndpoints();
|
final endpoints = _visibleEndpoints();
|
||||||
final distanceUnit = isImperial ? 'mi' : 'km';
|
final distanceUnit = isImperial ? 'mi' : 'km';
|
||||||
final heightUnit = isImperial ? 'ft' : 'm';
|
final heightUnit = isImperial ? 'ft' : 'm';
|
||||||
@@ -463,31 +474,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (segment != null)
|
if (segment != null)
|
||||||
SizedBox(
|
_buildProfileView(segment, distanceUnit, heightUnit, isImperial)
|
||||||
height: 160,
|
|
||||||
width: double.infinity,
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: _LosProfilePainter(
|
|
||||||
samples: segment.samples,
|
|
||||||
distanceUnit: distanceUnit,
|
|
||||||
heightUnit: heightUnit,
|
|
||||||
badgeTextStyle:
|
|
||||||
Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
||||||
color: Colors.white70,
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
) ??
|
|
||||||
const TextStyle(
|
|
||||||
color: Colors.white70,
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
terrainLabel: context.l10n.losLegendTerrain,
|
|
||||||
losBeamLabel: context.l10n.losLegendLosBeam,
|
|
||||||
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 44,
|
height: 44,
|
||||||
@@ -519,6 +506,96 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (obstructions.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
context.l10n.losBlockedSpotsTitle,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
context.l10n.losBlockedSpotsHint,
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: [
|
||||||
|
for (final obstruction in obstructions)
|
||||||
|
ChoiceChip(
|
||||||
|
label: Text(
|
||||||
|
_obstructionChipLabel(obstruction, isImperial),
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
),
|
||||||
|
selected:
|
||||||
|
_selectedObstruction?.sampleIndex ==
|
||||||
|
obstruction.sampleIndex,
|
||||||
|
onSelected: (_) => _selectObstruction(obstruction),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_selectedObstruction != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.deepOrangeAccent.withValues(alpha: 0.45),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.l10n.losSelectedObstructionTitle,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
context.l10n.losSelectedObstructionDetails(
|
||||||
|
_formatHeightValue(
|
||||||
|
_selectedObstruction!.obstructionMeters,
|
||||||
|
isImperial,
|
||||||
|
),
|
||||||
|
heightUnit,
|
||||||
|
_formatDistanceValue(
|
||||||
|
_selectedObstruction!.distanceMeters,
|
||||||
|
isImperial,
|
||||||
|
),
|
||||||
|
distanceUnit,
|
||||||
|
_formatDistanceValue(
|
||||||
|
segment!.totalDistanceMeters -
|
||||||
|
_selectedObstruction!.distanceMeters,
|
||||||
|
isImperial,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${_selectedObstruction!.point.latitude.toStringAsFixed(5)}, '
|
||||||
|
'${_selectedObstruction!.point.longitude.toStringAsFixed(5)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (displayFrequencyMHz != null)
|
if (displayFrequencyMHz != null)
|
||||||
Padding(
|
Padding(
|
||||||
@@ -605,6 +682,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
_showDisplayNodes = value;
|
_showDisplayNodes = value;
|
||||||
_sanitizeSelection();
|
_sanitizeSelection();
|
||||||
_result = null;
|
_result = null;
|
||||||
|
_selectedObstruction = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -655,6 +733,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_start = value;
|
_start = value;
|
||||||
_result = null;
|
_result = null;
|
||||||
|
_selectedObstruction = null;
|
||||||
});
|
});
|
||||||
if (_start != null && _end != null) {
|
if (_start != null && _end != null) {
|
||||||
_runLos();
|
_runLos();
|
||||||
@@ -670,6 +749,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_end = value;
|
_end = value;
|
||||||
_result = null;
|
_result = null;
|
||||||
|
_selectedObstruction = null;
|
||||||
});
|
});
|
||||||
if (_start != null && _end != null) {
|
if (_start != null && _end != null) {
|
||||||
_runLos();
|
_runLos();
|
||||||
@@ -769,6 +849,179 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
return _result!.segments.first.result;
|
return _result!.segments.first.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<LineOfSightObstruction> _primaryObstructions() {
|
||||||
|
return _primarySegmentResult()?.obstructions ?? const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
LineOfSightObstruction? _defaultObstructionFor(
|
||||||
|
LineOfSightPathResult? result,
|
||||||
|
) {
|
||||||
|
if (result == null || result.segments.isEmpty) return null;
|
||||||
|
final obstructions = result.segments.first.result.obstructions;
|
||||||
|
if (obstructions.isEmpty) return null;
|
||||||
|
return obstructions.reduce(
|
||||||
|
(current, next) =>
|
||||||
|
next.obstructionMeters > current.obstructionMeters ? next : current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectObstruction(LineOfSightObstruction obstruction) {
|
||||||
|
setState(() {
|
||||||
|
_selectedObstruction = obstruction;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDistanceValue(double meters, bool isImperial) {
|
||||||
|
final value = isImperial ? (meters / 1000.0) * _kmToMiles : meters / 1000.0;
|
||||||
|
return value.toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatHeightValue(double meters, bool isImperial) {
|
||||||
|
final value = isImperial ? meters * _metersToFeet : meters;
|
||||||
|
return value.toStringAsFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _obstructionChipLabel(
|
||||||
|
LineOfSightObstruction obstruction,
|
||||||
|
bool isImperial,
|
||||||
|
) {
|
||||||
|
final distanceUnit = isImperial ? 'mi' : 'km';
|
||||||
|
final heightUnit = isImperial ? 'ft' : 'm';
|
||||||
|
return context.l10n.losBlockedSpotChip(
|
||||||
|
_formatDistanceValue(obstruction.distanceMeters, isImperial),
|
||||||
|
distanceUnit,
|
||||||
|
_formatHeightValue(obstruction.obstructionMeters, isImperial),
|
||||||
|
heightUnit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProfileView(
|
||||||
|
LineOfSightResult segment,
|
||||||
|
String distanceUnit,
|
||||||
|
String heightUnit,
|
||||||
|
bool isImperial,
|
||||||
|
) {
|
||||||
|
if (segment.samples.length < 2) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 160,
|
||||||
|
width: double.infinity,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _LosProfilePainter(
|
||||||
|
samples: segment.samples,
|
||||||
|
distanceUnit: distanceUnit,
|
||||||
|
heightUnit: heightUnit,
|
||||||
|
badgeTextStyle:
|
||||||
|
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
) ??
|
||||||
|
const TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
terrainLabel: context.l10n.losLegendTerrain,
|
||||||
|
losBeamLabel: context.l10n.losLegendLosBeam,
|
||||||
|
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
|
||||||
|
selectedSampleIndex: _selectedObstruction?.sampleIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
height: 160,
|
||||||
|
width: double.infinity,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final size = Size(constraints.maxWidth, 160);
|
||||||
|
final geometry = _LosProfileGeometry(
|
||||||
|
samples: segment.samples,
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _LosProfilePainter(
|
||||||
|
samples: segment.samples,
|
||||||
|
distanceUnit: distanceUnit,
|
||||||
|
heightUnit: heightUnit,
|
||||||
|
badgeTextStyle:
|
||||||
|
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
) ??
|
||||||
|
const TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
terrainLabel: context.l10n.losLegendTerrain,
|
||||||
|
losBeamLabel: context.l10n.losLegendLosBeam,
|
||||||
|
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
|
||||||
|
selectedSampleIndex: _selectedObstruction?.sampleIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final obstruction in segment.obstructions)
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final sample = segment.samples[obstruction.sampleIndex];
|
||||||
|
final position = geometry.mapPoint(
|
||||||
|
sample.distanceMeters,
|
||||||
|
sample.terrainMeters,
|
||||||
|
);
|
||||||
|
final isSelected =
|
||||||
|
_selectedObstruction?.sampleIndex ==
|
||||||
|
obstruction.sampleIndex;
|
||||||
|
final markerSize = isSelected ? 18.0 : 14.0;
|
||||||
|
final left = (position.dx - markerSize / 2)
|
||||||
|
.clamp(0.0, math.max(0.0, size.width - markerSize))
|
||||||
|
.toDouble();
|
||||||
|
final top = (position.dy - markerSize / 2)
|
||||||
|
.clamp(0.0, math.max(0.0, size.height - markerSize))
|
||||||
|
.toDouble();
|
||||||
|
return Positioned(
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
child: Tooltip(
|
||||||
|
message: _obstructionChipLabel(obstruction, isImperial),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => _selectObstruction(obstruction),
|
||||||
|
child: Container(
|
||||||
|
width: markerSize,
|
||||||
|
height: markerSize,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? Colors.amberAccent
|
||||||
|
: Colors.deepOrangeAccent,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black87,
|
||||||
|
width: isSelected ? 2 : 1.5,
|
||||||
|
),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(color: Colors.black45, blurRadius: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String _profileStats(LineOfSightResult result, bool isImperial) {
|
String _profileStats(LineOfSightResult result, bool isImperial) {
|
||||||
final distance = isImperial
|
final distance = isImperial
|
||||||
? (result.totalDistanceMeters / 1000.0) * _kmToMiles
|
? (result.totalDistanceMeters / 1000.0) * _kmToMiles
|
||||||
@@ -820,8 +1073,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
return polylines;
|
return polylines;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Marker> _buildMarkers(List<LineOfSightEndpoint> endpoints) {
|
List<Marker> _buildMarkers(
|
||||||
|
List<LineOfSightEndpoint> endpoints,
|
||||||
|
List<LineOfSightObstruction> obstructions,
|
||||||
|
) {
|
||||||
return [
|
return [
|
||||||
|
for (final obstruction in obstructions)
|
||||||
|
Marker(
|
||||||
|
point: obstruction.point,
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => _selectObstruction(obstruction),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width:
|
||||||
|
_selectedObstruction?.sampleIndex == obstruction.sampleIndex
|
||||||
|
? 36
|
||||||
|
: 24,
|
||||||
|
height:
|
||||||
|
_selectedObstruction?.sampleIndex == obstruction.sampleIndex
|
||||||
|
? 36
|
||||||
|
: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.transparent,
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
_selectedObstruction?.sampleIndex ==
|
||||||
|
obstruction.sampleIndex
|
||||||
|
? Colors.amberAccent
|
||||||
|
: Colors.deepOrangeAccent,
|
||||||
|
width:
|
||||||
|
_selectedObstruction?.sampleIndex ==
|
||||||
|
obstruction.sampleIndex
|
||||||
|
? 4
|
||||||
|
: 3,
|
||||||
|
),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(color: Colors.black26, blurRadius: 6),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
for (final endpoint in endpoints)
|
for (final endpoint in endpoints)
|
||||||
Marker(
|
Marker(
|
||||||
point: endpoint.point,
|
point: endpoint.point,
|
||||||
@@ -1010,6 +1306,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _LosProfileGeometry {
|
||||||
|
static const horizontalPadding = 12.0;
|
||||||
|
static const verticalPadding = 12.0;
|
||||||
|
|
||||||
|
final List<LineOfSightSample> samples;
|
||||||
|
final Size size;
|
||||||
|
late final double minY = samples
|
||||||
|
.map(
|
||||||
|
(s) => math.min(
|
||||||
|
math.min(s.terrainMeters, s.lineHeightMeters),
|
||||||
|
s.refractedHeightMeters,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.reduce(math.min);
|
||||||
|
late final double maxY = samples
|
||||||
|
.map(
|
||||||
|
(s) => math.max(
|
||||||
|
math.max(s.terrainMeters, s.lineHeightMeters),
|
||||||
|
s.refractedHeightMeters,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.reduce(math.max);
|
||||||
|
late final double ySpan = math.max(1.0, maxY - minY);
|
||||||
|
late final double maxDist = math.max(1.0, samples.last.distanceMeters);
|
||||||
|
late final double chartWidth = math.max(
|
||||||
|
1.0,
|
||||||
|
size.width - horizontalPadding * 2,
|
||||||
|
);
|
||||||
|
late final double chartHeight = math.max(
|
||||||
|
1.0,
|
||||||
|
size.height - verticalPadding * 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
_LosProfileGeometry({required this.samples, required this.size});
|
||||||
|
|
||||||
|
Offset mapPoint(double distanceMeters, double elevationMeters) {
|
||||||
|
final px = horizontalPadding + (distanceMeters / maxDist) * chartWidth;
|
||||||
|
final py =
|
||||||
|
size.height -
|
||||||
|
verticalPadding -
|
||||||
|
((elevationMeters - minY) / ySpan) * chartHeight;
|
||||||
|
return Offset(px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _LosProfilePainter extends CustomPainter {
|
class _LosProfilePainter extends CustomPainter {
|
||||||
final List<LineOfSightSample> samples;
|
final List<LineOfSightSample> samples;
|
||||||
final String distanceUnit;
|
final String distanceUnit;
|
||||||
@@ -1018,6 +1359,7 @@ class _LosProfilePainter extends CustomPainter {
|
|||||||
final String terrainLabel;
|
final String terrainLabel;
|
||||||
final String losBeamLabel;
|
final String losBeamLabel;
|
||||||
final String radioHorizonLabel;
|
final String radioHorizonLabel;
|
||||||
|
final int? selectedSampleIndex;
|
||||||
|
|
||||||
const _LosProfilePainter({
|
const _LosProfilePainter({
|
||||||
required this.samples,
|
required this.samples,
|
||||||
@@ -1027,6 +1369,7 @@ class _LosProfilePainter extends CustomPainter {
|
|||||||
required this.terrainLabel,
|
required this.terrainLabel,
|
||||||
required this.losBeamLabel,
|
required this.losBeamLabel,
|
||||||
required this.radioHorizonLabel,
|
required this.radioHorizonLabel,
|
||||||
|
this.selectedSampleIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1212,6 +1555,32 @@ class _LosProfilePainter extends CustomPainter {
|
|||||||
..color = horizonFillColor
|
..color = horizonFillColor
|
||||||
..style = PaintingStyle.fill,
|
..style = PaintingStyle.fill,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (selectedSampleIndex != null &&
|
||||||
|
selectedSampleIndex! >= 0 &&
|
||||||
|
selectedSampleIndex! < samples.length) {
|
||||||
|
final selectedSample = samples[selectedSampleIndex!];
|
||||||
|
final selectedPoint = mapPoint(
|
||||||
|
selectedSample.distanceMeters,
|
||||||
|
selectedSample.terrainMeters,
|
||||||
|
);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(selectedPoint.dx, verticalPadding),
|
||||||
|
Offset(selectedPoint.dx, size.height - verticalPadding),
|
||||||
|
Paint()
|
||||||
|
..color = Colors.amberAccent.withValues(alpha: 0.7)
|
||||||
|
..strokeWidth = 1.5,
|
||||||
|
);
|
||||||
|
canvas.drawCircle(selectedPoint, 7, Paint()..color = Colors.amberAccent);
|
||||||
|
canvas.drawCircle(
|
||||||
|
selectedPoint,
|
||||||
|
8.5,
|
||||||
|
Paint()
|
||||||
|
..color = Colors.white
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 1.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1222,7 +1591,8 @@ class _LosProfilePainter extends CustomPainter {
|
|||||||
oldDelegate.badgeTextStyle != badgeTextStyle ||
|
oldDelegate.badgeTextStyle != badgeTextStyle ||
|
||||||
oldDelegate.terrainLabel != terrainLabel ||
|
oldDelegate.terrainLabel != terrainLabel ||
|
||||||
oldDelegate.losBeamLabel != losBeamLabel ||
|
oldDelegate.losBeamLabel != losBeamLabel ||
|
||||||
oldDelegate.radioHorizonLabel != radioHorizonLabel;
|
oldDelegate.radioHorizonLabel != radioHorizonLabel ||
|
||||||
|
oldDelegate.selectedSampleIndex != selectedSampleIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawUnitBadge(Canvas canvas, Size size) {
|
void _drawUnitBadge(Canvas canvas, Size size) {
|
||||||
|
|||||||
+173
-52
@@ -38,6 +38,7 @@ import 'line_of_sight_map_screen.dart';
|
|||||||
class MapScreen extends StatefulWidget {
|
class MapScreen extends StatefulWidget {
|
||||||
final LatLng? highlightPosition;
|
final LatLng? highlightPosition;
|
||||||
final String? highlightLabel;
|
final String? highlightLabel;
|
||||||
|
final String? highlightMarkerKey;
|
||||||
final double highlightZoom;
|
final double highlightZoom;
|
||||||
final bool hideBackButton;
|
final bool hideBackButton;
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ class MapScreen extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.highlightPosition,
|
this.highlightPosition,
|
||||||
this.highlightLabel,
|
this.highlightLabel,
|
||||||
|
this.highlightMarkerKey,
|
||||||
this.highlightZoom = 15.0,
|
this.highlightZoom = 15.0,
|
||||||
this.hideBackButton = false,
|
this.hideBackButton = false,
|
||||||
});
|
});
|
||||||
@@ -95,6 +97,19 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
_removedMarkerIds = ids;
|
_removedMarkerIds = ids;
|
||||||
_removedMarkersLoaded = true;
|
_removedMarkersLoaded = true;
|
||||||
});
|
});
|
||||||
|
// If this screen was opened to highlight a marker, and that marker
|
||||||
|
// was previously removed, re-enable it now that we've loaded the saved
|
||||||
|
// removed IDs.
|
||||||
|
if (widget.highlightMarkerKey != null &&
|
||||||
|
_removedMarkerIds.contains(widget.highlightMarkerKey)) {
|
||||||
|
final updated = Set<String>.from(_removedMarkerIds);
|
||||||
|
updated.remove(widget.highlightMarkerKey);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_removedMarkerIds = updated;
|
||||||
|
});
|
||||||
|
await _markerService.saveRemovedIds(updated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _checkLocationPlausibility(double lat, double lon) {
|
bool _checkLocationPlausibility(double lat, double lon) {
|
||||||
@@ -230,6 +245,24 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
: <Polyline>[],
|
: <Polyline>[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Collect polylines for shared markers' history with dashed lines
|
||||||
|
final List<Polyline> sharedMarkerPolylines = [];
|
||||||
|
for (final marker in sharedMarkers) {
|
||||||
|
if (marker.history.isNotEmpty) {
|
||||||
|
final points = List<LatLng>.from(marker.history);
|
||||||
|
points.add(marker.position);
|
||||||
|
sharedMarkerPolylines.add(
|
||||||
|
Polyline(
|
||||||
|
points: points,
|
||||||
|
color: marker.isChannel
|
||||||
|
? (marker.isPublicChannel ? Colors.orange : Colors.purple)
|
||||||
|
: Colors.blue,
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate center and zoom of all nodes, or default to (0, 0)
|
// Calculate center and zoom of all nodes, or default to (0, 0)
|
||||||
LatLng center = const LatLng(0, 0);
|
LatLng center = const LatLng(0, 0);
|
||||||
double initialZoom = 10.0;
|
double initialZoom = 10.0;
|
||||||
@@ -476,6 +509,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
if (_polylines.isNotEmpty && _isBuildingPathTrace)
|
if (_polylines.isNotEmpty && _isBuildingPathTrace)
|
||||||
PolylineLayer(polylines: _polylines),
|
PolylineLayer(polylines: _polylines),
|
||||||
|
if (sharedMarkerPolylines.isNotEmpty)
|
||||||
|
PolylineLayer(polylines: sharedMarkerPolylines),
|
||||||
MarkerLayer(
|
MarkerLayer(
|
||||||
markers: [
|
markers: [
|
||||||
if (highlightPosition != null)
|
if (highlightPosition != null)
|
||||||
@@ -1240,28 +1275,39 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) {
|
List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) {
|
||||||
final markers = <_SharedMarker>[];
|
// Build a _SharedMarker per message (history empty), grouped by dedupe key.
|
||||||
|
// Afterwards pick the latest per key and fill its history from older ones.
|
||||||
|
final updatesByKey = <String, List<_SharedMarker>>{};
|
||||||
final selfName = connector.selfName ?? 'Me';
|
final selfName = connector.selfName ?? 'Me';
|
||||||
|
|
||||||
|
void addUpdate(_SharedMarker update) {
|
||||||
|
(updatesByKey[update.id] ??= <_SharedMarker>[]).add(update);
|
||||||
|
}
|
||||||
|
|
||||||
for (final contact in connector.contacts) {
|
for (final contact in connector.contacts) {
|
||||||
final messages = connector.getMessages(contact);
|
final messages = connector.getMessages(contact);
|
||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
final payload = _parseMarkerText(message.text);
|
final payload = parseMarkerText(message.text);
|
||||||
if (payload == null) continue;
|
if (payload == null) continue;
|
||||||
final fromName = message.isOutgoing ? selfName : contact.name;
|
final fromName = message.isOutgoing ? selfName : contact.name;
|
||||||
final id = _buildMarkerId(
|
final key = buildSharedMarkerKey(
|
||||||
sourceId: contact.publicKeyHex,
|
sourceId: contact.publicKeyHex,
|
||||||
timestamp: message.timestamp,
|
label: payload.label,
|
||||||
text: message.text,
|
fromName: fromName,
|
||||||
|
flags: payload.flags,
|
||||||
|
isChannel: false,
|
||||||
);
|
);
|
||||||
markers.add(
|
addUpdate(
|
||||||
_SharedMarker(
|
_SharedMarker(
|
||||||
id: id,
|
id: key,
|
||||||
position: payload.position,
|
position: payload.position,
|
||||||
label: payload.label,
|
label: payload.label.isEmpty
|
||||||
|
? context.l10n.map_sharedPin
|
||||||
|
: payload.label,
|
||||||
flags: payload.flags,
|
flags: payload.flags,
|
||||||
fromName: fromName,
|
fromName: fromName,
|
||||||
sourceLabel: contact.name,
|
sourceLabel: contact.name,
|
||||||
|
timestamp: message.timestamp,
|
||||||
isChannel: false,
|
isChannel: false,
|
||||||
isPublicChannel: false,
|
isPublicChannel: false,
|
||||||
),
|
),
|
||||||
@@ -1273,23 +1319,28 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
final isPublic = _isPublicChannel(channel);
|
final isPublic = _isPublicChannel(channel);
|
||||||
final messages = connector.getChannelMessages(channel);
|
final messages = connector.getChannelMessages(channel);
|
||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
final payload = _parseMarkerText(message.text);
|
final payload = parseMarkerText(message.text);
|
||||||
if (payload == null) continue;
|
if (payload == null) continue;
|
||||||
final id = _buildMarkerId(
|
final key = buildSharedMarkerKey(
|
||||||
sourceId: 'channel:${channel.index}',
|
sourceId: 'channel:${channel.index}',
|
||||||
timestamp: message.timestamp,
|
label: payload.label,
|
||||||
text: message.text,
|
fromName: message.senderName,
|
||||||
|
flags: payload.flags,
|
||||||
|
isChannel: true,
|
||||||
);
|
);
|
||||||
markers.add(
|
addUpdate(
|
||||||
_SharedMarker(
|
_SharedMarker(
|
||||||
id: id,
|
id: key,
|
||||||
position: payload.position,
|
position: payload.position,
|
||||||
label: payload.label,
|
label: payload.label.isEmpty
|
||||||
|
? context.l10n.map_sharedPin
|
||||||
|
: payload.label,
|
||||||
flags: payload.flags,
|
flags: payload.flags,
|
||||||
fromName: message.senderName,
|
fromName: message.senderName,
|
||||||
sourceLabel: channel.name.isEmpty
|
sourceLabel: channel.name.isEmpty
|
||||||
? 'Channel ${channel.index}'
|
? 'Channel ${channel.index}'
|
||||||
: channel.name,
|
: channel.name,
|
||||||
|
timestamp: message.timestamp,
|
||||||
isChannel: true,
|
isChannel: true,
|
||||||
isPublicChannel: isPublic,
|
isPublicChannel: isPublic,
|
||||||
),
|
),
|
||||||
@@ -1297,38 +1348,27 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final markers = <_SharedMarker>[];
|
||||||
|
updatesByKey.forEach((_, updates) {
|
||||||
|
updates.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
final latest = updates.last;
|
||||||
|
// History: older positions, drop consecutive duplicates at same position.
|
||||||
|
final history = <LatLng>[];
|
||||||
|
for (var i = 0; i < updates.length - 1; i++) {
|
||||||
|
final p = updates[i].position;
|
||||||
|
if (history.isEmpty ||
|
||||||
|
history.last.latitude != p.latitude ||
|
||||||
|
history.last.longitude != p.longitude) {
|
||||||
|
history.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
markers.add(latest.copyWithHistory(history));
|
||||||
|
});
|
||||||
|
|
||||||
|
markers.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||||
return markers;
|
return markers;
|
||||||
}
|
}
|
||||||
|
|
||||||
_MarkerPayload? _parseMarkerText(String text) {
|
|
||||||
final trimmed = text.trim();
|
|
||||||
if (!trimmed.startsWith('m:')) return null;
|
|
||||||
|
|
||||||
final parts = trimmed.substring(2).split('|');
|
|
||||||
if (parts.isEmpty) return null;
|
|
||||||
final coords = parts[0].split(',');
|
|
||||||
if (coords.length != 2) return null;
|
|
||||||
final lat = double.tryParse(coords[0].trim());
|
|
||||||
final lon = double.tryParse(coords[1].trim());
|
|
||||||
if (lat == null || lon == null) return null;
|
|
||||||
|
|
||||||
final label = parts.length > 1 ? parts[1].trim() : '';
|
|
||||||
final flags = parts.length > 2 ? parts[2].trim() : '';
|
|
||||||
return _MarkerPayload(
|
|
||||||
position: LatLng(lat, lon),
|
|
||||||
label: label.isEmpty ? context.l10n.map_sharedPin : label,
|
|
||||||
flags: flags,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _buildMarkerId({
|
|
||||||
required String sourceId,
|
|
||||||
required DateTime timestamp,
|
|
||||||
required String text,
|
|
||||||
}) {
|
|
||||||
return '$sourceId|${timestamp.millisecondsSinceEpoch}|$text';
|
|
||||||
}
|
|
||||||
|
|
||||||
Marker _buildSharedMarker(_SharedMarker marker) {
|
Marker _buildSharedMarker(_SharedMarker marker) {
|
||||||
final markerColor = marker.isChannel
|
final markerColor = marker.isChannel
|
||||||
? (marker.isPublicChannel ? Colors.orange : Colors.purple)
|
? (marker.isPublicChannel ? Colors.orange : Colors.purple)
|
||||||
@@ -1338,7 +1378,15 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => _showMarkerInfo(marker),
|
onTap: () async {
|
||||||
|
if (_removedMarkerIds.contains(marker.id)) {
|
||||||
|
setState(() {
|
||||||
|
_removedMarkerIds.remove(marker.id);
|
||||||
|
});
|
||||||
|
await _markerService.saveRemovedIds(_removedMarkerIds);
|
||||||
|
}
|
||||||
|
_showMarkerInfo(marker);
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -1392,11 +1440,17 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
room: room,
|
room: room,
|
||||||
// onLogin(password, isAdmin) isAdmin not used for room caht screen
|
// onLogin(password, isAdmin) isAdmin not used for room caht screen
|
||||||
onLogin: (password, _) {
|
onLogin: (password, _) {
|
||||||
// Navigate to chat screen after successful login
|
final connector = context.read<MeshCoreConnector>();
|
||||||
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
final unread = connector.getUnreadCountForContactKey(
|
||||||
|
room.publicKeyHex,
|
||||||
|
);
|
||||||
|
connector.markContactRead(room.publicKeyHex);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: room)),
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
ChatScreen(contact: room, initialUnreadCount: unread),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1457,11 +1511,17 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
if (!contact.isActive) {
|
if (!contact.isActive) {
|
||||||
connector.importDiscoveredContact(contact);
|
connector.importDiscoveredContact(contact);
|
||||||
}
|
}
|
||||||
|
final unread = connector.getUnreadCountForContactKey(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
);
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ChatScreen(contact: contact),
|
builder: (context) => ChatScreen(
|
||||||
|
contact: contact,
|
||||||
|
initialUnreadCount: unread,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1543,13 +1603,19 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: Text(marker.label),
|
title: Text(
|
||||||
|
marker.label.isEmpty ? context.l10n.map_sharedPin : marker.label,
|
||||||
|
),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoRow(context.l10n.map_from, marker.fromName),
|
_buildInfoRow(context.l10n.map_from, marker.fromName),
|
||||||
_buildInfoRow(context.l10n.map_source, marker.sourceLabel),
|
_buildInfoRow(context.l10n.map_source, marker.sourceLabel),
|
||||||
|
_buildInfoRow(
|
||||||
|
context.l10n.map_sharedAt,
|
||||||
|
_formatLastSeen(marker.timestamp),
|
||||||
|
),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context.l10n.map_location,
|
context.l10n.map_location,
|
||||||
'${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}',
|
'${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}',
|
||||||
@@ -1716,6 +1782,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
String defaultLabel,
|
String defaultLabel,
|
||||||
) async {
|
) async {
|
||||||
final controller = TextEditingController(text: defaultLabel);
|
final controller = TextEditingController(text: defaultLabel);
|
||||||
|
controller.selection = TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: controller.text.length,
|
||||||
|
);
|
||||||
return showDialog<String>(
|
return showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
@@ -2311,18 +2381,50 @@ class _GuessedLocation {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MarkerPayload {
|
class MarkerPayload {
|
||||||
final LatLng position;
|
final LatLng position;
|
||||||
final String label;
|
final String label;
|
||||||
final String flags;
|
final String flags;
|
||||||
|
|
||||||
_MarkerPayload({
|
MarkerPayload({
|
||||||
required this.position,
|
required this.position,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.flags,
|
required this.flags,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a shared marker text message of the form
|
||||||
|
/// `m:<lat>,<lon>|<label>|<flags>` and return a [MarkerPayload].
|
||||||
|
MarkerPayload? parseMarkerText(String text) {
|
||||||
|
final trimmed = text.trim();
|
||||||
|
final match = RegExp(
|
||||||
|
r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|(.*)',
|
||||||
|
).firstMatch(trimmed);
|
||||||
|
if (match == null) return null;
|
||||||
|
final lat = double.tryParse(match.group(1) ?? '');
|
||||||
|
final lon = double.tryParse(match.group(2) ?? '');
|
||||||
|
if (lat == null || lon == null) return null;
|
||||||
|
final label = (match.group(3) ?? '').trim();
|
||||||
|
final flags = (match.group(4) ?? '').trim();
|
||||||
|
return MarkerPayload(position: LatLng(lat, lon), label: label, flags: flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a normalized dedupe key for shared markers.
|
||||||
|
/// Keeps the same algorithm previously present in both chat and map screens.
|
||||||
|
String buildSharedMarkerKey({
|
||||||
|
required String sourceId,
|
||||||
|
required String label,
|
||||||
|
required String fromName,
|
||||||
|
required String flags,
|
||||||
|
required bool isChannel,
|
||||||
|
}) {
|
||||||
|
final normalizedLabel = label.trim().toLowerCase();
|
||||||
|
final normalizedFrom = fromName.trim().toLowerCase();
|
||||||
|
final normalizedFlags = flags.trim().toLowerCase();
|
||||||
|
final scope = isChannel ? 'ch' : 'dm';
|
||||||
|
return '$scope|$sourceId|$normalizedFrom|$normalizedLabel|$normalizedFlags';
|
||||||
|
}
|
||||||
|
|
||||||
class _SharedMarker {
|
class _SharedMarker {
|
||||||
final String id;
|
final String id;
|
||||||
final LatLng position;
|
final LatLng position;
|
||||||
@@ -2330,8 +2432,10 @@ class _SharedMarker {
|
|||||||
final String flags;
|
final String flags;
|
||||||
final String fromName;
|
final String fromName;
|
||||||
final String sourceLabel;
|
final String sourceLabel;
|
||||||
|
final DateTime timestamp;
|
||||||
final bool isChannel;
|
final bool isChannel;
|
||||||
final bool isPublicChannel;
|
final bool isPublicChannel;
|
||||||
|
final List<LatLng> history;
|
||||||
|
|
||||||
_SharedMarker({
|
_SharedMarker({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -2340,7 +2444,24 @@ class _SharedMarker {
|
|||||||
required this.flags,
|
required this.flags,
|
||||||
required this.fromName,
|
required this.fromName,
|
||||||
required this.sourceLabel,
|
required this.sourceLabel,
|
||||||
|
required this.timestamp,
|
||||||
required this.isChannel,
|
required this.isChannel,
|
||||||
required this.isPublicChannel,
|
required this.isPublicChannel,
|
||||||
|
this.history = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_SharedMarker copyWithHistory(List<LatLng> newHistory) {
|
||||||
|
return _SharedMarker(
|
||||||
|
id: id,
|
||||||
|
position: position,
|
||||||
|
label: label,
|
||||||
|
flags: flags,
|
||||||
|
fromName: fromName,
|
||||||
|
sourceLabel: sourceLabel,
|
||||||
|
timestamp: timestamp,
|
||||||
|
isChannel: isChannel,
|
||||||
|
isPublicChannel: isPublicChannel,
|
||||||
|
history: newHistory,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,26 @@ class LineOfSightSample {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LineOfSightObstruction {
|
||||||
|
final int sampleIndex;
|
||||||
|
final LatLng point;
|
||||||
|
final double distanceMeters;
|
||||||
|
final double clearanceMeters;
|
||||||
|
final double obstructionMeters;
|
||||||
|
final double terrainMeters;
|
||||||
|
final double lineHeightMeters;
|
||||||
|
|
||||||
|
const LineOfSightObstruction({
|
||||||
|
required this.sampleIndex,
|
||||||
|
required this.point,
|
||||||
|
required this.distanceMeters,
|
||||||
|
required this.clearanceMeters,
|
||||||
|
required this.obstructionMeters,
|
||||||
|
required this.terrainMeters,
|
||||||
|
required this.lineHeightMeters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class LineOfSightResult {
|
class LineOfSightResult {
|
||||||
final bool hasData;
|
final bool hasData;
|
||||||
final bool isClear;
|
final bool isClear;
|
||||||
@@ -31,6 +51,7 @@ class LineOfSightResult {
|
|||||||
final double maxObstructionMeters;
|
final double maxObstructionMeters;
|
||||||
final double? firstObstructionDistanceMeters;
|
final double? firstObstructionDistanceMeters;
|
||||||
final List<LineOfSightSample> samples;
|
final List<LineOfSightSample> samples;
|
||||||
|
final List<LineOfSightObstruction> obstructions;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final double usedKFactor;
|
final double usedKFactor;
|
||||||
final double? frequencyMHz;
|
final double? frequencyMHz;
|
||||||
@@ -42,6 +63,7 @@ class LineOfSightResult {
|
|||||||
required this.maxObstructionMeters,
|
required this.maxObstructionMeters,
|
||||||
required this.firstObstructionDistanceMeters,
|
required this.firstObstructionDistanceMeters,
|
||||||
required this.samples,
|
required this.samples,
|
||||||
|
required this.obstructions,
|
||||||
required this.usedKFactor,
|
required this.usedKFactor,
|
||||||
this.frequencyMHz,
|
this.frequencyMHz,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
@@ -56,7 +78,8 @@ class LineOfSightResult {
|
|||||||
isClear = false,
|
isClear = false,
|
||||||
maxObstructionMeters = 0,
|
maxObstructionMeters = 0,
|
||||||
firstObstructionDistanceMeters = null,
|
firstObstructionDistanceMeters = null,
|
||||||
samples = const [];
|
samples = const [],
|
||||||
|
obstructions = const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
class LineOfSightPathSegment {
|
class LineOfSightPathSegment {
|
||||||
@@ -191,6 +214,7 @@ class LineOfSightService {
|
|||||||
maxObstructionMeters: 0,
|
maxObstructionMeters: 0,
|
||||||
firstObstructionDistanceMeters: null,
|
firstObstructionDistanceMeters: null,
|
||||||
samples: const [],
|
samples: const [],
|
||||||
|
obstructions: const [],
|
||||||
usedKFactor: kFactor,
|
usedKFactor: kFactor,
|
||||||
frequencyMHz: frequencyMHz,
|
frequencyMHz: frequencyMHz,
|
||||||
);
|
);
|
||||||
@@ -249,7 +273,9 @@ class LineOfSightService {
|
|||||||
var maxObstructionMeters = 0.0;
|
var maxObstructionMeters = 0.0;
|
||||||
double? firstObstructionDistanceMeters;
|
double? firstObstructionDistanceMeters;
|
||||||
final samples = <LineOfSightSample>[];
|
final samples = <LineOfSightSample>[];
|
||||||
|
final obstructions = <LineOfSightObstruction>[];
|
||||||
var isClear = true;
|
var isClear = true;
|
||||||
|
LineOfSightObstruction? clusterWorstObstruction;
|
||||||
|
|
||||||
for (int i = 0; i < points.length; i++) {
|
for (int i = 0; i < points.length; i++) {
|
||||||
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
|
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
|
||||||
@@ -274,6 +300,23 @@ class LineOfSightService {
|
|||||||
maxObstructionMeters = obstruction;
|
maxObstructionMeters = obstruction;
|
||||||
}
|
}
|
||||||
firstObstructionDistanceMeters ??= distanceFromStart;
|
firstObstructionDistanceMeters ??= distanceFromStart;
|
||||||
|
final candidate = LineOfSightObstruction(
|
||||||
|
sampleIndex: i,
|
||||||
|
point: points[i],
|
||||||
|
distanceMeters: distanceFromStart,
|
||||||
|
clearanceMeters: clearance,
|
||||||
|
obstructionMeters: obstruction,
|
||||||
|
terrainMeters: terrainHeight,
|
||||||
|
lineHeightMeters: lineHeight,
|
||||||
|
);
|
||||||
|
if (clusterWorstObstruction == null ||
|
||||||
|
candidate.obstructionMeters >
|
||||||
|
clusterWorstObstruction.obstructionMeters) {
|
||||||
|
clusterWorstObstruction = candidate;
|
||||||
|
}
|
||||||
|
} else if (clusterWorstObstruction != null) {
|
||||||
|
obstructions.add(clusterWorstObstruction);
|
||||||
|
clusterWorstObstruction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
samples.add(
|
samples.add(
|
||||||
@@ -286,6 +329,9 @@ class LineOfSightService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (clusterWorstObstruction != null) {
|
||||||
|
obstructions.add(clusterWorstObstruction);
|
||||||
|
}
|
||||||
|
|
||||||
return LineOfSightResult(
|
return LineOfSightResult(
|
||||||
hasData: true,
|
hasData: true,
|
||||||
@@ -294,6 +340,7 @@ class LineOfSightService {
|
|||||||
maxObstructionMeters: maxObstructionMeters,
|
maxObstructionMeters: maxObstructionMeters,
|
||||||
firstObstructionDistanceMeters: firstObstructionDistanceMeters,
|
firstObstructionDistanceMeters: firstObstructionDistanceMeters,
|
||||||
samples: samples,
|
samples: samples,
|
||||||
|
obstructions: obstructions,
|
||||||
usedKFactor: kFactor,
|
usedKFactor: kFactor,
|
||||||
frequencyMHz: frequencyMHz,
|
frequencyMHz: frequencyMHz,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,447 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// MeshCore redesign palette — warm field-journal dark theme with
|
||||||
|
/// phosphor-green signal accents. Mirrors values from the redesign spec.
|
||||||
|
class MeshPalette {
|
||||||
|
MeshPalette._();
|
||||||
|
|
||||||
|
// Surfaces (warm near-black, olive undertone)
|
||||||
|
static const bg = Color(0xFF0F1412);
|
||||||
|
static const bg1 = Color(0xFF161C19);
|
||||||
|
static const bg2 = Color(0xFF1D2521);
|
||||||
|
static const bg3 = Color(0xFF28322D);
|
||||||
|
static const bg4 = Color(0xFF34403A);
|
||||||
|
|
||||||
|
// Lines
|
||||||
|
static const line = Color(0xFF232C28);
|
||||||
|
static const line2 = Color(0xFF34403A);
|
||||||
|
static const line3 = Color(0xFF48564F);
|
||||||
|
|
||||||
|
// Ink
|
||||||
|
static const ink = Color(0xFFEFF3E8);
|
||||||
|
static const ink2 = Color(0xFFBAC4B5);
|
||||||
|
static const ink3 = Color(0xFF7C8B82);
|
||||||
|
static const ink4 = Color(0xFF55635B);
|
||||||
|
|
||||||
|
// Signal (phosphor)
|
||||||
|
static const signal = Color(0xFF7BEFA8);
|
||||||
|
static const signalDim = Color(0xFF4DC580);
|
||||||
|
static const signalBg = Color(0x177BEFA8); // ~9% alpha
|
||||||
|
static const signalLine = Color(0x427BEFA8); // ~26%
|
||||||
|
static const signalGlow = Color(0x597BEFA8); // ~35%
|
||||||
|
|
||||||
|
// Warn (ember)
|
||||||
|
static const warn = Color(0xFFFFA552);
|
||||||
|
static const warnDim = Color(0xFFC27E3C);
|
||||||
|
static const warnBg = Color(0x1CFFA552);
|
||||||
|
static const warnLine = Color(0x4DFFA552);
|
||||||
|
|
||||||
|
// Alert (coral)
|
||||||
|
static const alert = Color(0xFFFF6A5C);
|
||||||
|
static const alertBg = Color(0x1CFF6A5C);
|
||||||
|
static const alertLine = Color(0x52FF6A5C);
|
||||||
|
|
||||||
|
// Blue (dusk sky)
|
||||||
|
static const blue = Color(0xFF7FCBF5);
|
||||||
|
static const blueBg = Color(0x1C7FCBF5);
|
||||||
|
static const blueLine = Color(0x477FCBF5);
|
||||||
|
|
||||||
|
// Magenta
|
||||||
|
static const magenta = Color(0xFFDE7FDB);
|
||||||
|
static const magentaBg = Color(0x1CDE7FDB);
|
||||||
|
static const magentaLine = Color(0x47DE7FDB);
|
||||||
|
|
||||||
|
// Me bubble (mossy)
|
||||||
|
static const me = Color(0xFF1E3527);
|
||||||
|
static const meBorder = Color(0xFF2D5039);
|
||||||
|
static const meInk = Color(0xFFDEF0DC);
|
||||||
|
|
||||||
|
// ── Light variant (used when user explicitly picks light theme)
|
||||||
|
static const lightBg = Color(0xFFF5F3EC);
|
||||||
|
static const lightBg1 = Color(0xFFECE9DF);
|
||||||
|
static const lightBg2 = Color(0xFFE2DED2);
|
||||||
|
static const lightLine = Color(0xFFCAC5B4);
|
||||||
|
static const lightInk = Color(0xFF0F1410);
|
||||||
|
static const lightInk2 = Color(0xFF3D463E);
|
||||||
|
static const lightInk3 = Color(0xFF6A756D);
|
||||||
|
static const lightSignal = Color(0xFF1A7A44);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Named font stacks — Flutter falls back to system fonts when the named
|
||||||
|
/// family isn't installed, keeping things working without bundled assets.
|
||||||
|
class MeshFonts {
|
||||||
|
MeshFonts._();
|
||||||
|
|
||||||
|
static const sans = 'Inter';
|
||||||
|
static const mono = 'JetBrains Mono';
|
||||||
|
static const display = 'Instrument Serif';
|
||||||
|
|
||||||
|
static const List<String> sansFallback = [
|
||||||
|
'system-ui',
|
||||||
|
'-apple-system',
|
||||||
|
'Roboto',
|
||||||
|
'Noto Sans',
|
||||||
|
'sans-serif',
|
||||||
|
];
|
||||||
|
static const List<String> monoFallback = [
|
||||||
|
'SF Mono',
|
||||||
|
'Menlo',
|
||||||
|
'Consolas',
|
||||||
|
'Roboto Mono',
|
||||||
|
'monospace',
|
||||||
|
];
|
||||||
|
static const List<String> displayFallback = [
|
||||||
|
'Cormorant Garamond',
|
||||||
|
'Georgia',
|
||||||
|
'Times New Roman',
|
||||||
|
'serif',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Radii used consistently across the app.
|
||||||
|
class MeshRadii {
|
||||||
|
MeshRadii._();
|
||||||
|
static const xs = 6.0;
|
||||||
|
static const sm = 10.0;
|
||||||
|
static const md = 14.0;
|
||||||
|
static const lg = 18.0;
|
||||||
|
static const xl = 24.0;
|
||||||
|
static const pill = 999.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared helpers exposed via [MeshTheme.of].
|
||||||
|
class MeshTheme {
|
||||||
|
MeshTheme._();
|
||||||
|
|
||||||
|
static ThemeData dark() {
|
||||||
|
const scheme = ColorScheme.dark(
|
||||||
|
primary: MeshPalette.signal,
|
||||||
|
onPrimary: Color(0xFF0A1810),
|
||||||
|
primaryContainer: MeshPalette.signalBg,
|
||||||
|
onPrimaryContainer: MeshPalette.signal,
|
||||||
|
secondary: MeshPalette.blue,
|
||||||
|
onSecondary: Color(0xFF0A1520),
|
||||||
|
tertiary: MeshPalette.magenta,
|
||||||
|
onTertiary: Color(0xFF201020),
|
||||||
|
error: MeshPalette.alert,
|
||||||
|
onError: Color(0xFF1A0A08),
|
||||||
|
errorContainer: MeshPalette.alertBg,
|
||||||
|
onErrorContainer: MeshPalette.alert,
|
||||||
|
surface: MeshPalette.bg,
|
||||||
|
onSurface: MeshPalette.ink,
|
||||||
|
surfaceContainerLowest: MeshPalette.bg,
|
||||||
|
surfaceContainerLow: MeshPalette.bg1,
|
||||||
|
surfaceContainer: MeshPalette.bg1,
|
||||||
|
surfaceContainerHigh: MeshPalette.bg2,
|
||||||
|
surfaceContainerHighest: MeshPalette.bg3,
|
||||||
|
onSurfaceVariant: MeshPalette.ink2,
|
||||||
|
outline: MeshPalette.line2,
|
||||||
|
outlineVariant: MeshPalette.line,
|
||||||
|
shadow: Colors.black,
|
||||||
|
scrim: Colors.black54,
|
||||||
|
inverseSurface: MeshPalette.ink,
|
||||||
|
onInverseSurface: MeshPalette.bg,
|
||||||
|
inversePrimary: MeshPalette.signalDim,
|
||||||
|
);
|
||||||
|
return _build(scheme, Brightness.dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ThemeData light() {
|
||||||
|
const scheme = ColorScheme.light(
|
||||||
|
primary: MeshPalette.lightSignal,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
primaryContainer: Color(0xFFD4E8D8),
|
||||||
|
onPrimaryContainer: MeshPalette.lightSignal,
|
||||||
|
secondary: Color(0xFF2F6EA8),
|
||||||
|
onSecondary: Colors.white,
|
||||||
|
tertiary: Color(0xFF8C4A8A),
|
||||||
|
onTertiary: Colors.white,
|
||||||
|
error: Color(0xFFB53D2F),
|
||||||
|
onError: Colors.white,
|
||||||
|
surface: MeshPalette.lightBg,
|
||||||
|
onSurface: MeshPalette.lightInk,
|
||||||
|
surfaceContainerLowest: MeshPalette.lightBg,
|
||||||
|
surfaceContainerLow: MeshPalette.lightBg1,
|
||||||
|
surfaceContainer: MeshPalette.lightBg1,
|
||||||
|
surfaceContainerHigh: MeshPalette.lightBg2,
|
||||||
|
surfaceContainerHighest: Color(0xFFD5D0C0),
|
||||||
|
onSurfaceVariant: MeshPalette.lightInk2,
|
||||||
|
outline: MeshPalette.lightLine,
|
||||||
|
outlineVariant: Color(0xFFDBD6C6),
|
||||||
|
);
|
||||||
|
return _build(scheme, Brightness.light);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ThemeData _build(ColorScheme scheme, Brightness brightness) {
|
||||||
|
final baseText =
|
||||||
|
Typography.material2021(
|
||||||
|
platform: TargetPlatform.android,
|
||||||
|
colorScheme: scheme,
|
||||||
|
).black.apply(
|
||||||
|
bodyColor: scheme.onSurface,
|
||||||
|
displayColor: scheme.onSurface,
|
||||||
|
fontFamily: MeshFonts.sans,
|
||||||
|
fontFamilyFallback: MeshFonts.sansFallback,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: brightness,
|
||||||
|
colorScheme: scheme,
|
||||||
|
scaffoldBackgroundColor: scheme.surface,
|
||||||
|
canvasColor: scheme.surface,
|
||||||
|
fontFamily: MeshFonts.sans,
|
||||||
|
fontFamilyFallback: MeshFonts.sansFallback,
|
||||||
|
textTheme: baseText,
|
||||||
|
dividerColor: scheme.outlineVariant,
|
||||||
|
dividerTheme: DividerThemeData(
|
||||||
|
color: scheme.outlineVariant,
|
||||||
|
thickness: 1,
|
||||||
|
space: 1,
|
||||||
|
),
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
backgroundColor: scheme.surface,
|
||||||
|
foregroundColor: scheme.onSurface,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
centerTitle: false,
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
fontFamily: MeshFonts.sans,
|
||||||
|
fontFamilyFallback: MeshFonts.sansFallback,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
color: scheme.onSurface,
|
||||||
|
),
|
||||||
|
iconTheme: IconThemeData(color: scheme.onSurface),
|
||||||
|
shape: Border(
|
||||||
|
bottom: BorderSide(color: scheme.outlineVariant, width: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
color: scheme.surfaceContainerLow,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||||
|
side: BorderSide(color: scheme.outlineVariant, width: 1),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0),
|
||||||
|
),
|
||||||
|
listTileTheme: ListTileThemeData(
|
||||||
|
iconColor: scheme.onSurfaceVariant,
|
||||||
|
textColor: scheme.onSurface,
|
||||||
|
tileColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
|
backgroundColor: scheme.primary,
|
||||||
|
foregroundColor: scheme.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
focusElevation: 0,
|
||||||
|
hoverElevation: 0,
|
||||||
|
highlightElevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||||
|
),
|
||||||
|
extendedTextStyle: const TextStyle(
|
||||||
|
fontFamily: MeshFonts.sans,
|
||||||
|
fontFamilyFallback: MeshFonts.sansFallback,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: scheme.primary,
|
||||||
|
foregroundColor: scheme.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||||
|
),
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontFamily: MeshFonts.sans,
|
||||||
|
fontFamilyFallback: MeshFonts.sansFallback,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: scheme.onSurface,
|
||||||
|
side: BorderSide(color: scheme.outline),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textButtonTheme: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: scheme.primary,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: scheme.surfaceContainerHigh,
|
||||||
|
hintStyle: TextStyle(color: scheme.onSurfaceVariant),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||||
|
borderSide: BorderSide(color: scheme.outline),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||||
|
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||||
|
borderSide: BorderSide(color: scheme.primary, width: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
chipTheme: ChipThemeData(
|
||||||
|
backgroundColor: scheme.surfaceContainerLow,
|
||||||
|
side: BorderSide(color: scheme.outlineVariant),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
fontFamily: MeshFonts.sans,
|
||||||
|
fontFamilyFallback: MeshFonts.sansFallback,
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
),
|
||||||
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
|
backgroundColor: scheme.surfaceContainerLow,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
indicatorColor: scheme.primary.withValues(alpha: 0.14),
|
||||||
|
indicatorShape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||||
|
),
|
||||||
|
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||||
|
final selected = states.contains(WidgetState.selected);
|
||||||
|
return TextStyle(
|
||||||
|
fontFamily: MeshFonts.mono,
|
||||||
|
fontFamilyFallback: MeshFonts.monoFallback,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
color: selected ? scheme.primary : scheme.onSurfaceVariant,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
iconTheme: WidgetStateProperty.resolveWith((states) {
|
||||||
|
final selected = states.contains(WidgetState.selected);
|
||||||
|
return IconThemeData(
|
||||||
|
color: selected ? scheme.primary : scheme.onSurfaceVariant,
|
||||||
|
size: 22,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
|
backgroundColor: scheme.surfaceContainerLow,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
modalBackgroundColor: scheme.surfaceContainerLow,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(MeshRadii.lg),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dialogTheme: DialogThemeData(
|
||||||
|
backgroundColor: scheme.surfaceContainerLow,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.lg),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
backgroundColor: scheme.surfaceContainerHigh,
|
||||||
|
contentTextStyle: TextStyle(color: scheme.onSurface),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
popupMenuTheme: PopupMenuThemeData(
|
||||||
|
color: scheme.surfaceContainerHigh,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
iconTheme: IconThemeData(color: scheme.onSurfaceVariant, size: 22),
|
||||||
|
splashFactory: InkSparkle.splashFactory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mono text style — sizes default to the body size Inter is using.
|
||||||
|
static TextStyle mono({
|
||||||
|
double? fontSize,
|
||||||
|
FontWeight? fontWeight,
|
||||||
|
Color? color,
|
||||||
|
double? letterSpacing,
|
||||||
|
}) {
|
||||||
|
return TextStyle(
|
||||||
|
fontFamily: MeshFonts.mono,
|
||||||
|
fontFamilyFallback: MeshFonts.monoFallback,
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: fontWeight,
|
||||||
|
color: color,
|
||||||
|
letterSpacing: letterSpacing ?? 0.2,
|
||||||
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serif display style.
|
||||||
|
static TextStyle display({
|
||||||
|
double? fontSize,
|
||||||
|
FontWeight? fontWeight,
|
||||||
|
Color? color,
|
||||||
|
double? letterSpacing,
|
||||||
|
}) {
|
||||||
|
return TextStyle(
|
||||||
|
fontFamily: MeshFonts.display,
|
||||||
|
fontFamilyFallback: MeshFonts.displayFallback,
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: fontWeight ?? FontWeight.w400,
|
||||||
|
color: color,
|
||||||
|
letterSpacing: letterSpacing ?? -0.2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Small-caps mono label — used for section accents and chip labels.
|
||||||
|
static TextStyle accentLabel({Color? color, double? fontSize}) {
|
||||||
|
return TextStyle(
|
||||||
|
fontFamily: MeshFonts.mono,
|
||||||
|
fontFamilyFallback: MeshFonts.monoFallback,
|
||||||
|
fontSize: fontSize ?? 9.5,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 1.8,
|
||||||
|
color: color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color-code an SNR value for consistency across the app.
|
||||||
|
static Color snrColor(num? snr, {required bool blocked}) {
|
||||||
|
if (blocked) return MeshPalette.alert;
|
||||||
|
if (snr == null) return MeshPalette.ink3;
|
||||||
|
if (snr > -5) return MeshPalette.signal;
|
||||||
|
if (snr > -12) return MeshPalette.warn;
|
||||||
|
return MeshPalette.alert;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
|
class UnreadDivider extends StatelessWidget {
|
||||||
|
const UnreadDivider({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = Theme.of(context).colorScheme.primary;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Divider(color: color)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
context.l10n.chat_newMessages,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: Divider(color: color)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ 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)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ void main() {
|
|||||||
expect(result.isClear, isTrue);
|
expect(result.isClear, isTrue);
|
||||||
expect(result.maxObstructionMeters, equals(0));
|
expect(result.maxObstructionMeters, equals(0));
|
||||||
expect(result.firstObstructionDistanceMeters, isNull);
|
expect(result.firstObstructionDistanceMeters, isNull);
|
||||||
|
expect(result.obstructions, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@@ -44,9 +45,32 @@ void main() {
|
|||||||
expect(result.isClear, isFalse);
|
expect(result.isClear, isFalse);
|
||||||
expect(result.maxObstructionMeters, greaterThan(0));
|
expect(result.maxObstructionMeters, greaterThan(0));
|
||||||
expect(result.firstObstructionDistanceMeters, isNotNull);
|
expect(result.firstObstructionDistanceMeters, isNotNull);
|
||||||
|
expect(result.obstructions, hasLength(1));
|
||||||
|
expect(result.obstructions.single.sampleIndex, equals(10));
|
||||||
|
expect(result.obstructions.single.point, equals(points[10]));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test('computeFromElevations groups contiguous blocked samples', () {
|
||||||
|
final points = makePoints(21);
|
||||||
|
final elevations = List<double>.filled(points.length, 100);
|
||||||
|
elevations[9] = 220;
|
||||||
|
elevations[10] = 320;
|
||||||
|
elevations[11] = 240;
|
||||||
|
|
||||||
|
final result = LineOfSightService.computeFromElevations(
|
||||||
|
points: points,
|
||||||
|
elevations: elevations,
|
||||||
|
startAntennaHeightMeters: 1.5,
|
||||||
|
endAntennaHeightMeters: 1.5,
|
||||||
|
kFactor: 4.0 / 3.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.obstructions, hasLength(1));
|
||||||
|
expect(result.obstructions.single.sampleIndex, equals(10));
|
||||||
|
expect(result.obstructions.single.obstructionMeters, greaterThan(0));
|
||||||
|
});
|
||||||
|
|
||||||
test('analyzePath summarizes clear and blocked segments', () async {
|
test('analyzePath summarizes clear and blocked segments', () async {
|
||||||
final service = LineOfSightService(
|
final service = LineOfSightService(
|
||||||
elevationDataSource: (points) async {
|
elevationDataSource: (points) async {
|
||||||
|
|||||||
+86
-1
@@ -1 +1,86 @@
|
|||||||
{}
|
{
|
||||||
|
"bg": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"de": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"es": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fr": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"hu": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"it": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ja": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ko": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"nl": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"pl": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"pt": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ru": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"sk": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"sl": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"sv": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"uk": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
],
|
||||||
|
|
||||||
|
"zh": [
|
||||||
|
"chat_markAsUnread",
|
||||||
|
"chat_newMessages"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ 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