Files
meshcore-sar_android/lib/widgets/map/drawing_toolbar.dart
2026-03-06 20:12:12 +01:00

1103 lines
41 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../providers/drawing_provider.dart';
import '../../providers/connection_provider.dart';
import '../../providers/contacts_provider.dart';
import '../../providers/messages_provider.dart';
import '../../models/map_drawing.dart';
import '../../models/contact.dart';
import '../../models/message.dart';
import '../../l10n/app_localizations.dart';
import '../../utils/toast_logger.dart';
import '../drawing_minimap_preview.dart';
/// Toolbar for drawing controls on the map
class DrawingToolbar extends StatelessWidget {
const DrawingToolbar({super.key});
@override
Widget build(BuildContext context) {
return Consumer<DrawingProvider>(
builder: (context, drawingProvider, _) {
if (!drawingProvider.isDrawing) {
// Show compact floating button when not in drawing mode
return FloatingActionButton.small(
heroTag: 'drawing_tool',
onPressed: () => _showDrawingMenu(context, drawingProvider),
child: const Icon(Icons.edit),
);
}
// Show full toolbar when in drawing mode
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Title bar with close button
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getIconForMode(drawingProvider.drawingMode),
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
_getTitleForMode(drawingProvider.drawingMode, context),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () => drawingProvider.exitDrawingMode(),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
const SizedBox(height: 8),
// Color picker - more compact
Wrap(
spacing: 4,
runSpacing: 4,
children: DrawingColors.palette.map((color) {
final isSelected = drawingProvider.selectedColor == color;
return GestureDetector(
onTap: () => drawingProvider.setColor(color),
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: isSelected
? Colors.white
: Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
boxShadow: [
if (isSelected)
BoxShadow(
color: color.withValues(alpha: 0.5),
blurRadius: 4,
spreadRadius: 1,
),
],
),
child: isSelected
? const Icon(
Icons.check,
color: Colors.white,
size: 12,
)
: null,
),
);
}).toList(),
),
const SizedBox(height: 8),
// Action buttons
Row(
mainAxisSize: MainAxisSize.min,
children: [
// Cancel current drawing
if (drawingProvider.currentLinePoints.isNotEmpty ||
drawingProvider.rectangleStartPoint != null)
IconButton(
icon: const Icon(Icons.undo),
onPressed: () => drawingProvider.cancelCurrentDrawing(),
tooltip: AppLocalizations.of(context)!.cancel,
iconSize: 20,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
// Clear measurement
if (drawingProvider.drawingMode == DrawingMode.measure &&
drawingProvider.measurementPoint1 != null)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => drawingProvider.clearMeasurement(),
tooltip: AppLocalizations.of(context)!.clearMeasurement,
color: Colors.orange,
iconSize: 20,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
// Complete line drawing
if (drawingProvider.drawingMode == DrawingMode.line &&
drawingProvider.currentLinePoints.length >= 2)
IconButton(
icon: const Icon(Icons.check),
onPressed: () => drawingProvider.completeLine(),
tooltip: AppLocalizations.of(context)!.completeLine,
color: Colors.green,
iconSize: 20,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
// Clear all drawings
if (drawingProvider.drawings.isNotEmpty)
IconButton(
icon: const Icon(Icons.delete_sweep),
onPressed: () =>
_showClearAllDialog(context, drawingProvider),
tooltip: AppLocalizations.of(context)!.clearAll,
color: Colors.red,
iconSize: 20,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
],
),
],
),
);
},
);
}
/// Show drawing mode selection menu
void _showDrawingMenu(BuildContext context, DrawingProvider drawingProvider) {
// Capture root context for use after modal closes
final rootContext = context;
showModalBottomSheet(
context: context,
builder: (sheetContext) => SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
children: [
const Icon(Icons.edit),
const SizedBox(width: 12),
Text(
AppLocalizations.of(sheetContext)!.drawingTools,
style: Theme.of(sheetContext).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
],
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.show_chart),
title: Text(AppLocalizations.of(sheetContext)!.drawLine),
subtitle: Text(AppLocalizations.of(sheetContext)!.drawLineDesc),
onTap: () {
Navigator.pop(sheetContext);
drawingProvider.setDrawingMode(DrawingMode.line);
},
),
ListTile(
leading: const Icon(Icons.crop_square),
title: Text(AppLocalizations.of(sheetContext)!.drawRectangle),
subtitle: Text(
AppLocalizations.of(sheetContext)!.drawRectangleDesc,
),
onTap: () {
Navigator.pop(sheetContext);
drawingProvider.setDrawingMode(DrawingMode.rectangle);
},
),
ListTile(
leading: const Icon(Icons.straighten),
title: Text(AppLocalizations.of(sheetContext)!.measureDistance),
subtitle: Text(
AppLocalizations.of(sheetContext)!.measureDistanceDesc,
),
onTap: () {
Navigator.pop(sheetContext);
drawingProvider.setDrawingMode(DrawingMode.measure);
},
),
const Divider(),
// Toggle received drawings visibility
SwitchListTile(
secondary: Icon(
drawingProvider.showReceivedDrawings
? Icons.visibility
: Icons.visibility_off,
),
title: Text(
AppLocalizations.of(sheetContext)!.showReceivedDrawings,
),
subtitle: Text(
drawingProvider.showReceivedDrawings
? AppLocalizations.of(sheetContext)!.showingAllDrawings
: AppLocalizations.of(
sheetContext,
)!.showingOnlyYourDrawings,
),
value: drawingProvider.showReceivedDrawings,
onChanged: (value) {
drawingProvider.toggleReceivedDrawings();
},
),
// Toggle SAR markers visibility
SwitchListTile(
secondary: Icon(
drawingProvider.showSarMarkers
? Icons.pin_drop
: Icons.pin_drop_outlined,
),
title: Text(AppLocalizations.of(sheetContext)!.showSarMarkers),
subtitle: Text(
drawingProvider.showSarMarkers
? AppLocalizations.of(sheetContext)!.showingSarMarkers
: AppLocalizations.of(sheetContext)!.hidingSarMarkers,
),
value: drawingProvider.showSarMarkers,
onChanged: (value) {
drawingProvider.toggleSarMarkers();
},
),
// Drawings list with individual actions
if (drawingProvider.drawings.isNotEmpty) ...[
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
children: [
const Icon(Icons.layers, size: 20),
const SizedBox(width: 8),
Text(
AppLocalizations.of(
sheetContext,
)!.yourDrawingsCount(drawingProvider.drawings.length),
style: Theme.of(sheetContext).textTheme.titleSmall
?.copyWith(fontWeight: FontWeight.w600),
),
],
),
),
...drawingProvider.drawings.map((drawing) {
final isShared = drawing.isShared;
final typeStr = drawing is LineDrawing
? AppLocalizations.of(sheetContext)!.line
: AppLocalizations.of(sheetContext)!.rectangle;
final colorName = DrawingColors.colorToName(drawing.color);
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300, width: 1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
// Minimap preview
DrawingMinimapPreview(drawing: drawing),
const SizedBox(width: 12),
// Drawing info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
typeStr,
style: Theme.of(sheetContext)
.textTheme
.titleSmall
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: drawing.color,
shape: BoxShape.circle,
border: Border.all(
color: Colors.black26,
width: 1,
),
),
),
const SizedBox(width: 6),
Text(
colorName,
style: Theme.of(
sheetContext,
).textTheme.labelSmall,
),
if (isShared) ...[
const SizedBox(width: 8),
Icon(
Icons.check_circle,
size: 14,
color: Colors.green.shade700,
),
const SizedBox(width: 4),
Text(
AppLocalizations.of(sheetContext)!.shared,
style: Theme.of(sheetContext)
.textTheme
.labelSmall
?.copyWith(
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
),
],
],
),
],
),
),
// Action buttons
Row(
mainAxisSize: MainAxisSize.min,
children: [
// Share button
if (!isShared)
IconButton(
icon: const Icon(Icons.share, size: 20),
onPressed: () async {
Navigator.pop(sheetContext);
await Future.delayed(
const Duration(milliseconds: 100),
);
if (rootContext.mounted) {
_showShareSingleDrawingDialog(
rootContext,
drawingProvider,
drawing,
);
}
},
tooltip: 'Share',
color: Colors.blue,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
const SizedBox(width: 4),
// Delete button
IconButton(
icon: const Icon(Icons.delete, size: 20),
onPressed: () {
Navigator.pop(sheetContext);
_showDeleteSingleDrawingDialog(
rootContext,
drawingProvider,
drawing,
);
},
tooltip: 'Delete',
color: Colors.red,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
],
),
],
),
);
}),
const Divider(),
ListTile(
leading: const Icon(Icons.share, color: Colors.blue),
title: Text(AppLocalizations.of(sheetContext)!.shareDrawings),
subtitle: Text(
AppLocalizations.of(sheetContext)!.broadcastDrawingsToTeam(
drawingProvider.drawings.length,
drawingProvider.drawings.length > 1 ? 's' : '',
),
),
onTap: () async {
Navigator.pop(sheetContext);
// Small delay to ensure first bottom sheet is fully closed
// before opening the second one
await Future.delayed(const Duration(milliseconds: 100));
if (rootContext.mounted) {
_showShareDrawingsDialog(rootContext, drawingProvider);
}
},
),
ListTile(
leading: const Icon(Icons.delete_sweep, color: Colors.red),
title: Text(
AppLocalizations.of(sheetContext)!.clearAllDrawings,
),
subtitle: Text(
AppLocalizations.of(sheetContext)!.removeAllDrawings(
drawingProvider.drawings.length,
drawingProvider.drawings.length > 1 ? 's' : '',
),
),
onTap: () {
Navigator.pop(sheetContext);
_showClearAllDialog(rootContext, drawingProvider);
},
),
],
],
),
),
),
);
}
/// Show clear all confirmation dialog
void _showClearAllDialog(
BuildContext context,
DrawingProvider drawingProvider,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.clearAllDrawings),
content: Text(
AppLocalizations.of(context)!.deleteAllDrawingsConfirm(
drawingProvider.drawings.length,
drawingProvider.drawings.length > 1 ? 's' : '',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () {
Navigator.pop(context);
drawingProvider.clearAllDrawings();
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(AppLocalizations.of(context)!.clearAll),
),
],
),
);
}
/// Get icon for drawing mode
IconData _getIconForMode(DrawingMode mode) {
switch (mode) {
case DrawingMode.line:
return Icons.show_chart;
case DrawingMode.rectangle:
return Icons.crop_square;
case DrawingMode.measure:
return Icons.straighten;
case DrawingMode.none:
return Icons.edit;
}
}
/// Get title for drawing mode
String _getTitleForMode(DrawingMode mode, BuildContext context) {
switch (mode) {
case DrawingMode.line:
return AppLocalizations.of(context)!.drawLine;
case DrawingMode.rectangle:
return AppLocalizations.of(context)!.drawRectangle;
case DrawingMode.measure:
return AppLocalizations.of(context)!.measureDistance;
case DrawingMode.none:
return AppLocalizations.of(context)!.drawing;
}
}
/// Show share drawings dialog
void _showShareDrawingsDialog(
BuildContext context,
DrawingProvider drawingProvider,
) {
debugPrint('🎨 [DrawingToolbar] _showShareDrawingsDialog called');
// Read providers BEFORE any async operations or dialogs
// This ensures we have the correct BuildContext
final connectionProvider = Provider.of<ConnectionProvider>(
context,
listen: false,
);
final contactsProvider = Provider.of<ContactsProvider>(
context,
listen: false,
);
debugPrint(
' Connection status: ${connectionProvider.deviceInfo.isConnected}',
);
if (!connectionProvider.deviceInfo.isConnected) {
debugPrint(' ❌ Not connected - showing error toast');
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.notConnectedToDevice,
);
}
return;
}
// Get device name for sender identification
final senderName = connectionProvider.deviceInfo.selfName ?? 'Unknown';
// Filter drawings - only share unshared local drawings
final unsharedDrawings = drawingProvider.getUnsharedDrawings();
debugPrint(' Unshared drawings count: ${unsharedDrawings.length}');
debugPrint(' Total drawings count: ${drawingProvider.drawings.length}');
if (unsharedDrawings.isEmpty) {
debugPrint(' No unshared drawings - showing info toast');
if (context.mounted) {
ToastLogger.info(context, 'All drawings have already been shared');
}
return;
}
// Get available rooms
final rooms = contactsProvider.rooms;
debugPrint(' Available rooms: ${rooms.length}');
debugPrint(' Showing modal bottom sheet...');
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (sheetContext) => SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
children: [
const Icon(Icons.share),
const SizedBox(width: 12),
Expanded(
child: Text(
AppLocalizations.of(context)!.shareDrawingsCount(
unsharedDrawings.length,
unsharedDrawings.length > 1 ? 's' : '',
),
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
),
const Divider(),
// Option: Send to Public Channel
ListTile(
leading: const Icon(Icons.public, color: Colors.blue),
title: Text(AppLocalizations.of(context)!.publicChannel),
subtitle: Text(AppLocalizations.of(context)!.broadcastToAll),
onTap: () async {
debugPrint('📤 [DrawingToolbar] Public Channel tapped');
// Share BEFORE popping the navigator
await _shareDrawingsToChannel(
sheetContext,
unsharedDrawings,
connectionProvider,
senderName,
);
if (sheetContext.mounted) {
Navigator.pop(sheetContext);
}
},
),
// Option: Send to Room
if (rooms.isNotEmpty) ...[
...rooms.map(
(room) => ListTile(
leading: const Icon(
Icons.meeting_room,
color: Colors.green,
),
title: Text(room.advName),
subtitle: Text(
AppLocalizations.of(context)!.storedPermanently,
),
onTap: () async {
debugPrint(
'📤 [DrawingToolbar] Room ${room.advName} tapped',
);
// Share BEFORE popping the navigator
await _shareDrawingsToRoom(
sheetContext,
unsharedDrawings,
connectionProvider,
senderName,
room,
);
if (sheetContext.mounted) {
Navigator.pop(sheetContext);
}
},
),
),
],
],
),
),
),
);
}
/// Share drawings to public channel
Future<void> _shareDrawingsToChannel(
BuildContext context,
List<MapDrawing> drawings,
ConnectionProvider connectionProvider,
String senderName,
) async {
debugPrint('📤 [DrawingToolbar] _shareDrawingsToChannel called');
debugPrint(' Drawings to share: ${drawings.length}');
debugPrint(' Sender name: $senderName');
debugPrint(' Context mounted: ${context.mounted}');
if (!context.mounted) {
debugPrint('❌ Context not mounted, aborting');
return;
}
final drawingProvider = Provider.of<DrawingProvider>(
context,
listen: false,
);
final messagesProvider = Provider.of<MessagesProvider>(
context,
listen: false,
);
int successCount = 0;
int alreadyShared = 0;
for (final drawing in drawings) {
// Skip if already shared
if (drawing.isShared) {
alreadyShared++;
continue;
}
try {
debugPrint(' Creating message for drawing ${drawing.id}...');
// Sender name is no longer included in JSON - will be extracted from packet metadata
final message = drawingProvider.createDrawingBroadcastMessage(drawing);
debugPrint(
' Message created (${message.length} chars): ${message.substring(0, message.length > 100 ? 100 : message.length)}...',
);
// Create message ID and timestamp
final messageId =
'${DateTime.now().millisecondsSinceEpoch}_channel_drawing_sent';
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Get current device's public key (first 6 bytes)
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
// Create sent message object
final sentMessage = Message(
id: messageId,
messageType: MessageType.channel,
senderPublicKeyPrefix: senderPublicKeyPrefix,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: message,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
channelIdx: 0,
isDrawing: true,
drawingId: drawing.id,
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
debugPrint(' Sending to channel 0...');
await connectionProvider.sendChannelMessage(
channelIdx: 0,
text: message,
messageId: messageId,
);
debugPrint(' ✅ Sent successfully');
// Mark as shared after successful send
drawingProvider.markDrawingAsShared(drawing.id);
successCount++;
// Small delay between messages to avoid overwhelming the device
await Future.delayed(const Duration(milliseconds: 200));
} catch (e, stackTrace) {
debugPrint('❌ Failed to share drawing ${drawing.id}: $e');
debugPrint(' Stack trace: $stackTrace');
}
}
debugPrint(
' Share complete: $successCount/${drawings.length} sent, $alreadyShared already shared',
);
}
/// Share drawings to a specific room
Future<void> _shareDrawingsToRoom(
BuildContext context,
List<MapDrawing> drawings,
ConnectionProvider connectionProvider,
String senderName,
Contact room,
) async {
debugPrint('📤 [DrawingToolbar] _shareDrawingsToRoom called');
debugPrint(' Room: ${room.advName}');
debugPrint(' Drawings to share: ${drawings.length}');
debugPrint(' Sender name: $senderName');
debugPrint(' Context mounted: ${context.mounted}');
if (!context.mounted) {
debugPrint('❌ Context not mounted, aborting');
return;
}
final drawingProvider = Provider.of<DrawingProvider>(
context,
listen: false,
);
final messagesProvider = Provider.of<MessagesProvider>(
context,
listen: false,
);
int successCount = 0;
int alreadyShared = 0;
for (final drawing in drawings) {
// Skip if already shared
if (drawing.isShared) {
alreadyShared++;
continue;
}
try {
debugPrint(' Creating message for drawing ${drawing.id}...');
// Sender name is no longer included in JSON - will be extracted from packet metadata
final message = drawingProvider.createDrawingBroadcastMessage(drawing);
debugPrint(
' Message created (${message.length} chars): ${message.substring(0, message.length > 100 ? 100 : message.length)}...',
);
// Create message ID and timestamp
final messageId =
'${DateTime.now().millisecondsSinceEpoch}_contact_drawing_sent';
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
// Get current device's public key (first 6 bytes)
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
// Create sent message object
final sentMessage = Message(
id: messageId,
messageType: MessageType.contact,
senderPublicKeyPrefix: senderPublicKeyPrefix,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: timestamp,
text: message,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sending,
recipientPublicKey: room.publicKey,
isDrawing: true,
drawingId: drawing.id,
);
// Add to messages list with "sending" status
messagesProvider.addSentMessage(sentMessage);
debugPrint(' Sending to room ${room.advName}...');
await connectionProvider.sendTextMessage(
contactPublicKey: room.publicKey,
text: message,
messageId: messageId,
contact: room,
);
debugPrint(' ✅ Sent successfully');
// Mark as shared after successful send
drawingProvider.markDrawingAsShared(drawing.id);
successCount++;
// Small delay between messages to avoid overwhelming the device
await Future.delayed(const Duration(milliseconds: 200));
} catch (e, stackTrace) {
debugPrint(
'❌ Failed to share drawing ${drawing.id} to ${room.advName}: $e',
);
debugPrint(' Stack trace: $stackTrace');
}
}
debugPrint(
' Share complete: $successCount/${drawings.length} sent, $alreadyShared already shared',
);
}
/// Show share dialog for a single drawing
void _showShareSingleDrawingDialog(
BuildContext context,
DrawingProvider drawingProvider,
MapDrawing drawing,
) {
debugPrint(
'🎨 [DrawingToolbar] _showShareSingleDrawingDialog called for ${drawing.id}',
);
// Read providers BEFORE any async operations or dialogs
final connectionProvider = Provider.of<ConnectionProvider>(
context,
listen: false,
);
final contactsProvider = Provider.of<ContactsProvider>(
context,
listen: false,
);
if (!connectionProvider.deviceInfo.isConnected) {
debugPrint(' ❌ Not connected - showing error toast');
if (context.mounted) {
ToastLogger.error(
context,
AppLocalizations.of(context)!.notConnectedToDevice,
);
}
return;
}
// Get device name for sender identification
final senderName = connectionProvider.deviceInfo.selfName ?? 'Unknown';
// Get available rooms
final rooms = contactsProvider.rooms;
debugPrint(' Available rooms: ${rooms.length}');
// Capture root context for provider access after modal closes
final rootContext = context;
debugPrint(' Showing modal bottom sheet...');
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (sheetContext) => SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
children: [
const Icon(Icons.share),
const SizedBox(width: 12),
Expanded(
child: Text(
AppLocalizations.of(sheetContext)!.shareDrawing,
style: Theme.of(sheetContext).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
),
const Divider(),
// Option: Send to Public Channel
ListTile(
leading: const Icon(Icons.public, color: Colors.blue),
title: Text(AppLocalizations.of(sheetContext)!.publicChannel),
subtitle: Text(
AppLocalizations.of(sheetContext)!.shareWithAllNearbyDevices,
),
onTap: () async {
Navigator.pop(sheetContext);
await _shareDrawingsToChannel(
rootContext,
[drawing],
connectionProvider,
senderName,
);
},
),
// Option: Send to specific room
if (rooms.isNotEmpty) ...[
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
AppLocalizations.of(sheetContext)!.shareToRoom,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
),
...rooms.map((room) {
return ListTile(
leading: const Icon(
Icons.meeting_room,
color: Colors.orange,
),
title: Text(room.advName),
subtitle: Text(
AppLocalizations.of(
sheetContext,
)!.sendToPersistentStorage,
),
onTap: () async {
Navigator.pop(sheetContext);
await _shareDrawingsToRoom(
rootContext,
[drawing],
connectionProvider,
senderName,
room,
);
},
);
}),
],
],
),
),
),
);
}
/// Show delete confirmation dialog for a single drawing
void _showDeleteSingleDrawingDialog(
BuildContext context,
DrawingProvider drawingProvider,
MapDrawing drawing,
) {
final typeStr = drawing is LineDrawing
? AppLocalizations.of(context)!.line
: AppLocalizations.of(context)!.rectangle;
final colorName = DrawingColors.colorToName(drawing.color);
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(AppLocalizations.of(dialogContext)!.deleteDrawing),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(dialogContext)!.deleteDrawingConfirm),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
DrawingMinimapPreview(drawing: drawing),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
typeStr,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: drawing.color,
shape: BoxShape.circle,
border: Border.all(
color: Colors.black26,
width: 1,
),
),
),
const SizedBox(width: 6),
Text(
colorName,
style: const TextStyle(fontSize: 12),
),
],
),
],
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(AppLocalizations.of(dialogContext)!.cancel),
),
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
drawingProvider.removeDrawing(drawing.id);
ToastLogger.success(
context,
AppLocalizations.of(context)!.drawingDeleted,
);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(AppLocalizations.of(dialogContext)!.delete),
),
],
),
);
}
}