mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add custom cave map mode
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
import '../models/map_coordinate_space.dart';
|
||||
|
||||
/// Minimap preview widget for map drawings
|
||||
/// Renders a small 80x80px preview of a drawing on a map background
|
||||
@@ -52,8 +53,12 @@ class DrawingMinimapPreview extends StatelessWidget {
|
||||
final rectDrawing = drawing as RectangleDrawing;
|
||||
|
||||
// Add padding (10% on each side)
|
||||
final latDiff = (rectDrawing.bottomRight.latitude - rectDrawing.topLeft.latitude).abs();
|
||||
final lonDiff = (rectDrawing.bottomRight.longitude - rectDrawing.topLeft.longitude).abs();
|
||||
final latDiff =
|
||||
(rectDrawing.bottomRight.latitude - rectDrawing.topLeft.latitude)
|
||||
.abs();
|
||||
final lonDiff =
|
||||
(rectDrawing.bottomRight.longitude - rectDrawing.topLeft.longitude)
|
||||
.abs();
|
||||
final latPadding = latDiff * 0.1;
|
||||
final lonPadding = lonDiff * 0.1;
|
||||
|
||||
@@ -79,6 +84,12 @@ class DrawingMinimapPreview extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bounds = _calculateBounds();
|
||||
final isCustomMap = drawing.coordinateSpace == MapCoordinateSpace.customMap;
|
||||
final drawingPoints = drawing is LineDrawing
|
||||
? (drawing as LineDrawing).points
|
||||
: drawing is RectangleDrawing
|
||||
? (drawing as RectangleDrawing).corners
|
||||
: const <LatLng>[];
|
||||
|
||||
return Container(
|
||||
width: 80,
|
||||
@@ -86,26 +97,27 @@ class DrawingMinimapPreview extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade400,
|
||||
width: 1,
|
||||
),
|
||||
border: Border.all(color: Colors.grey.shade400, width: 1),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
child: flutter_map.FlutterMap(
|
||||
options: flutter_map.MapOptions(
|
||||
crs: isCustomMap
|
||||
? const flutter_map.CrsSimple()
|
||||
: const flutter_map.Epsg3857(),
|
||||
initialCameraFit: flutter_map.CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(8),
|
||||
),
|
||||
interactionOptions: const flutter_map.InteractionOptions(
|
||||
flags: flutter_map.InteractiveFlag.none, // Disable all interactions
|
||||
flags:
|
||||
flutter_map.InteractiveFlag.none, // Disable all interactions
|
||||
),
|
||||
),
|
||||
children: [
|
||||
// Use provided tile layer or fallback to gray background
|
||||
if (tileLayer != null)
|
||||
if (!isCustomMap && tileLayer != null)
|
||||
tileLayer!
|
||||
else
|
||||
Container(color: Colors.grey.shade300),
|
||||
@@ -115,7 +127,7 @@ class DrawingMinimapPreview extends StatelessWidget {
|
||||
flutter_map.PolylineLayer(
|
||||
polylines: [
|
||||
flutter_map.Polyline(
|
||||
points: (drawing as LineDrawing).points,
|
||||
points: drawingPoints,
|
||||
strokeWidth: 3.0,
|
||||
color: drawing.color,
|
||||
),
|
||||
@@ -125,7 +137,7 @@ class DrawingMinimapPreview extends StatelessWidget {
|
||||
flutter_map.PolygonLayer(
|
||||
polygons: [
|
||||
flutter_map.Polygon(
|
||||
points: (drawing as RectangleDrawing).corners,
|
||||
points: drawingPoints,
|
||||
color: drawing.color.withValues(alpha: 0.3),
|
||||
borderColor: drawing.color,
|
||||
borderStrokeWidth: 3.0,
|
||||
|
||||
@@ -8,8 +8,14 @@ import '../../l10n/app_localizations.dart';
|
||||
class DrawingLayer extends StatelessWidget {
|
||||
final List<MapDrawing> drawings;
|
||||
final MapDrawing? previewDrawing;
|
||||
final LatLng Function(LatLng point)? pointTransformer;
|
||||
|
||||
const DrawingLayer({super.key, required this.drawings, this.previewDrawing});
|
||||
const DrawingLayer({
|
||||
super.key,
|
||||
required this.drawings,
|
||||
this.previewDrawing,
|
||||
this.pointTransformer,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -66,12 +72,16 @@ class DrawingLayer extends StatelessWidget {
|
||||
/// Get points from a drawing based on its type
|
||||
List<LatLng> _getPoints(MapDrawing drawing) {
|
||||
if (drawing is LineDrawing) {
|
||||
return drawing.points;
|
||||
return drawing.points.map(_transformPoint).toList();
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
return drawing.corners;
|
||||
return drawing.corners.map(_transformPoint).toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
LatLng _transformPoint(LatLng point) {
|
||||
return pointTransformer?.call(point) ?? point;
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget that shows drawing markers (start/end points)
|
||||
@@ -80,6 +90,7 @@ class DrawingMarkersLayer extends StatelessWidget {
|
||||
final Function(String drawingId)? onDeleteDrawing;
|
||||
final Function(MapDrawing drawing)? onTapDrawing;
|
||||
final bool showDeleteButtons;
|
||||
final LatLng Function(LatLng point)? pointTransformer;
|
||||
|
||||
const DrawingMarkersLayer({
|
||||
super.key,
|
||||
@@ -87,6 +98,7 @@ class DrawingMarkersLayer extends StatelessWidget {
|
||||
this.onDeleteDrawing,
|
||||
this.onTapDrawing,
|
||||
this.showDeleteButtons = false,
|
||||
this.pointTransformer,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -144,17 +156,23 @@ class DrawingMarkersLayer extends StatelessWidget {
|
||||
if (drawing is LineDrawing && drawing.points.isNotEmpty) {
|
||||
// Use the middle point of the line
|
||||
final midIndex = drawing.points.length ~/ 2;
|
||||
return drawing.points[midIndex];
|
||||
return _transformPoint(drawing.points[midIndex]);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
// Use the center of the rectangle
|
||||
return LatLng(
|
||||
(drawing.topLeft.latitude + drawing.bottomRight.latitude) / 2,
|
||||
(drawing.topLeft.longitude + drawing.bottomRight.longitude) / 2,
|
||||
return _transformPoint(
|
||||
LatLng(
|
||||
(drawing.topLeft.latitude + drawing.bottomRight.latitude) / 2,
|
||||
(drawing.topLeft.longitude + drawing.bottomRight.longitude) / 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LatLng _transformPoint(LatLng point) {
|
||||
return pointTransformer?.call(point) ?? point;
|
||||
}
|
||||
|
||||
/// Show delete confirmation dialog
|
||||
void _showDeleteDialog(BuildContext context, MapDrawing drawing) {
|
||||
showDialog(
|
||||
|
||||
462
lib/widgets/messages/custom_map_sar_update_sheet.dart
Normal file
462
lib/widgets/messages/custom_map_sar_update_sheet.dart
Normal file
@@ -0,0 +1,462 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../models/contact.dart';
|
||||
import '../../models/sar_template.dart';
|
||||
import '../../providers/contacts_provider.dart';
|
||||
import '../../services/sar_template_service.dart';
|
||||
import 'sar_update_sheet.dart' show TemplateChip;
|
||||
|
||||
class CustomMapSarUpdateSheet extends StatefulWidget {
|
||||
final String mapName;
|
||||
final String mapId;
|
||||
final String pointLabel;
|
||||
final Future<void> Function(
|
||||
String emoji,
|
||||
String name,
|
||||
Uint8List? roomPublicKey,
|
||||
bool sendToChannel,
|
||||
bool sendToAllContacts,
|
||||
int colorIndex,
|
||||
)
|
||||
onSend;
|
||||
|
||||
const CustomMapSarUpdateSheet({
|
||||
super.key,
|
||||
required this.mapName,
|
||||
required this.mapId,
|
||||
required this.pointLabel,
|
||||
required this.onSend,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomMapSarUpdateSheet> createState() =>
|
||||
_CustomMapSarUpdateSheetState();
|
||||
}
|
||||
|
||||
class _CustomMapSarUpdateSheetState extends State<CustomMapSarUpdateSheet> {
|
||||
final SarTemplateService _templateService = SarTemplateService();
|
||||
final TextEditingController _notesController = TextEditingController();
|
||||
|
||||
List<SarTemplate> _templates = [];
|
||||
SarTemplate? _selectedTemplate;
|
||||
Contact? _selectedContact;
|
||||
bool _sendToAllContacts = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeTemplates();
|
||||
_setDefaultDestination();
|
||||
}
|
||||
|
||||
Future<void> _initializeTemplates() async {
|
||||
if (!_templateService.isInitialized) {
|
||||
await _templateService.initialize();
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_templates = _templateService.templates;
|
||||
if (_templates.isNotEmpty) {
|
||||
_selectedTemplate = _templates.first;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _setDefaultDestination() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final roomsAndChannels = contactsProvider.roomsAndChannels;
|
||||
final teamContacts = contactsProvider.chatContacts;
|
||||
|
||||
if (teamContacts.length > 1) {
|
||||
setState(() {
|
||||
_sendToAllContacts = true;
|
||||
_selectedContact = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (teamContacts.length == 1) {
|
||||
setState(() {
|
||||
_sendToAllContacts = false;
|
||||
_selectedContact = teamContacts.first;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomsAndChannels.any((c) => c.isRoom)) {
|
||||
setState(() {
|
||||
_sendToAllContacts = false;
|
||||
_selectedContact = roomsAndChannels.firstWhere((c) => c.isRoom);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomsAndChannels.isNotEmpty) {
|
||||
setState(() {
|
||||
_sendToAllContacts = false;
|
||||
_selectedContact = roomsAndChannels.first;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_notesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
final bottomSafeArea = MediaQuery.of(context).padding.bottom;
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return AnimatedPadding(
|
||||
padding: EdgeInsets.only(bottom: keyboardHeight),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height * 0.88,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Send SAR marker',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Custom cave map point',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 80 + bottomSafeArea,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.markerType,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
..._templates.map((template) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: TemplateChip(
|
||||
template: template,
|
||||
isSelected: _selectedTemplate?.id == template.id,
|
||||
onTap: () =>
|
||||
setState(() => _selectedTemplate = template),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.sendTo,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Consumer<ContactsProvider>(
|
||||
builder: (context, contactsProvider, child) {
|
||||
final teamContacts = contactsProvider.chatContacts;
|
||||
final roomsAndChannels =
|
||||
contactsProvider.roomsAndChannels;
|
||||
final destinations = <Contact>[
|
||||
...teamContacts,
|
||||
...roomsAndChannels.where((c) => c.isRoom),
|
||||
...roomsAndChannels.where((c) => c.isChannel),
|
||||
];
|
||||
|
||||
if (destinations.isEmpty && teamContacts.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.red.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: const Text('No destinations available'),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: _sendToAllContacts
|
||||
? 'all_contacts'
|
||||
: _selectedContact?.publicKeyHex,
|
||||
hint: Text(
|
||||
AppLocalizations.of(context)!.selectDestination,
|
||||
),
|
||||
dropdownColor:
|
||||
colorScheme.surfaceContainerHighest,
|
||||
isExpanded: true,
|
||||
items: [
|
||||
if (teamContacts.isNotEmpty)
|
||||
DropdownMenuItem<String>(
|
||||
value: 'all_contacts',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.group, size: 18),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.allTeamContacts,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
...destinations.map((contact) {
|
||||
final icon = contact.isChat
|
||||
? Icons.person
|
||||
: contact.isRoom
|
||||
? Icons.storage
|
||||
: Icons.public;
|
||||
return DropdownMenuItem<String>(
|
||||
value: contact.publicKeyHex,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
contact.getLocalizedDisplayName(
|
||||
context,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == 'all_contacts') {
|
||||
_sendToAllContacts = true;
|
||||
_selectedContact = null;
|
||||
} else {
|
||||
_sendToAllContacts = false;
|
||||
_selectedContact = destinations.firstWhere(
|
||||
(c) => c.publicKeyHex == value,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Map point',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.map_outlined,
|
||||
size: 20,
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.mapName,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.pointLabel,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Map ID: ${widget.mapId}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Additional notes (optional)',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _notesController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Add additional details',
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + bottomSafeArea),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: () async {
|
||||
if (_selectedTemplate == null ||
|
||||
(!_sendToAllContacts && _selectedContact == null)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Select marker type and destination'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await widget.onSend(
|
||||
_selectedTemplate!.emoji,
|
||||
_notesController.text.trim().isEmpty
|
||||
? _selectedTemplate!.name
|
||||
: _notesController.text.trim(),
|
||||
_selectedContact?.isChannel == true
|
||||
? null
|
||||
: _selectedContact?.publicKey,
|
||||
_selectedContact?.isChannel == true,
|
||||
_sendToAllContacts,
|
||||
_templates.indexOf(_selectedTemplate!),
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.send),
|
||||
label: const Text('Send'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../models/contact.dart';
|
||||
@@ -7,6 +8,7 @@ import '../../models/message.dart';
|
||||
import '../../models/sar_marker.dart';
|
||||
import '../../models/sar_template.dart';
|
||||
import '../../models/map_drawing.dart';
|
||||
import '../../models/map_coordinate_space.dart';
|
||||
import '../../providers/messages_provider.dart';
|
||||
import '../../providers/contacts_provider.dart';
|
||||
import '../../providers/connection_provider.dart';
|
||||
@@ -1490,7 +1492,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
|
||||
void _navigateToDrawing(BuildContext context) {
|
||||
if (widget.message.drawingId == null) return;
|
||||
widget.onNavigateToMap?.call();
|
||||
widget.onTap?.call();
|
||||
}
|
||||
|
||||
void _copyDrawingCoordinates(BuildContext context) {
|
||||
@@ -1505,21 +1507,18 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
}
|
||||
|
||||
// Format coordinates based on drawing type
|
||||
String formatPoint(LatLng point) {
|
||||
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
return '${point.latitude.toStringAsFixed(0)}, ${point.longitude.toStringAsFixed(0)}';
|
||||
}
|
||||
return '${point.latitude.toStringAsFixed(5)}, ${point.longitude.toStringAsFixed(5)}';
|
||||
}
|
||||
|
||||
String coordinatesText;
|
||||
if (drawing is LineDrawing) {
|
||||
coordinatesText = drawing.points
|
||||
.map(
|
||||
(p) =>
|
||||
'${p.latitude.toStringAsFixed(5)}, ${p.longitude.toStringAsFixed(5)}',
|
||||
)
|
||||
.join('\n');
|
||||
coordinatesText = drawing.points.map(formatPoint).join('\n');
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
coordinatesText = drawing.corners
|
||||
.map(
|
||||
(p) =>
|
||||
'${p.latitude.toStringAsFixed(5)}, ${p.longitude.toStringAsFixed(5)}',
|
||||
)
|
||||
.join('\n');
|
||||
coordinatesText = drawing.corners.map(formatPoint).join('\n');
|
||||
} else {
|
||||
ToastLogger.error(context, 'Unknown drawing type');
|
||||
return;
|
||||
@@ -2294,6 +2293,72 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
],
|
||||
),
|
||||
),
|
||||
] else if (message.sarCustomMapPoint != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(
|
||||
alpha: isDarkMode ? 0.18 : 0.05,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.map_outlined,
|
||||
size: 15,
|
||||
color: _getSarMarkerBorderColor(
|
||||
context,
|
||||
isDarkMode,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Custom map marker',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Point: ${message.sarCustomMapPoint!.latitude.toStringAsFixed(0)}, ${message.sarCustomMapPoint!.longitude.toStringAsFixed(0)}',
|
||||
style: Theme.of(context).textTheme.labelMedium
|
||||
?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.15,
|
||||
),
|
||||
),
|
||||
if (message.sarCustomMapId != null &&
|
||||
message.sarCustomMapId!.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Map ID: ${message.sarCustomMapId}',
|
||||
style: Theme.of(context).textTheme.labelMedium
|
||||
?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.15,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user