Resolve 62 linting issues identified by flutter analyze, reducing total issues from 79 to 17. All critical warnings addressed. Changes: - Replace deprecated withOpacity() with withValues(alpha:) (25 instances) - Remove unused imports from 9 files - Remove unused variables and fields (6 instances) - Fix Riverpod 3.0 state access violations in settings_page - Remove unnecessary null-aware operators in navigation_page (6 instances) - Fix unnecessary type casts in providers (4 instances) - Remove unused methods: _getDarkMapStyle, _showPermissionDialog - Simplify hover state management by removing unused _isHovered fields Remaining 17 issues are info-level style suggestions and defensive programming patterns that don't impact functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
787 lines
25 KiB
Dart
787 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:google_navigation_flutter/google_navigation_flutter.dart';
|
|
import '../models/delivery.dart';
|
|
import '../theme/color_system.dart';
|
|
|
|
/// Enhanced dark-mode aware map component with custom styling
|
|
class DarkModeMapComponent extends StatefulWidget {
|
|
final List<Delivery> deliveries;
|
|
final Delivery? selectedDelivery;
|
|
final ValueChanged<Delivery?>? onDeliverySelected;
|
|
final Function(String)? onAction;
|
|
|
|
const DarkModeMapComponent({
|
|
super.key,
|
|
required this.deliveries,
|
|
this.selectedDelivery,
|
|
this.onDeliverySelected,
|
|
this.onAction,
|
|
});
|
|
|
|
@override
|
|
State<DarkModeMapComponent> createState() => _DarkModeMapComponentState();
|
|
}
|
|
|
|
class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
|
|
GoogleNavigationViewController? _navigationController;
|
|
bool _isNavigating = false;
|
|
LatLng? _destinationLocation;
|
|
bool _isSessionInitialized = false;
|
|
bool _isInitializing = false;
|
|
bool _isStartingNavigation = false;
|
|
String _loadingMessage = 'Initializing...';
|
|
Brightness? _lastBrightness;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initializeNavigation();
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
|
|
// Detect theme changes and reapply map style
|
|
final currentBrightness = Theme.of(context).brightness;
|
|
if (_lastBrightness != null &&
|
|
_lastBrightness != currentBrightness &&
|
|
_navigationController != null) {
|
|
_applyDarkModeStyle();
|
|
}
|
|
_lastBrightness = currentBrightness;
|
|
}
|
|
|
|
Future<void> _initializeNavigation() async {
|
|
if (_isInitializing || _isSessionInitialized) return;
|
|
|
|
setState(() {
|
|
_isInitializing = true;
|
|
});
|
|
|
|
try {
|
|
final termsAccepted = await GoogleMapsNavigator.areTermsAccepted();
|
|
if (!termsAccepted) {
|
|
await GoogleMapsNavigator.showTermsAndConditionsDialog(
|
|
'Plan B Logistics',
|
|
'com.goutezplanb.planbLogistic',
|
|
);
|
|
}
|
|
await GoogleMapsNavigator.initializeNavigationSession();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isSessionInitialized = true;
|
|
_isInitializing = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
final errorMessage = _formatErrorMessage(e);
|
|
debugPrint('Map initialization error: $errorMessage');
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isInitializing = false;
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Navigation initialization failed: $errorMessage'),
|
|
duration: const Duration(seconds: 5),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
String _formatErrorMessage(Object error) {
|
|
final errorString = error.toString();
|
|
if (errorString.contains('SessionNotInitializedException')) {
|
|
return 'Google Maps navigation session could not be initialized';
|
|
} else if (errorString.contains('permission')) {
|
|
return 'Location permission is required for navigation';
|
|
} else if (errorString.contains('network')) {
|
|
return 'Network connection error';
|
|
}
|
|
return errorString;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(DarkModeMapComponent oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.selectedDelivery != widget.selectedDelivery) {
|
|
_updateDestination();
|
|
}
|
|
}
|
|
|
|
void _updateDestination() {
|
|
if (widget.selectedDelivery != null) {
|
|
final address = widget.selectedDelivery!.deliveryAddress;
|
|
if (address?.latitude != null && address?.longitude != null) {
|
|
setState(() {
|
|
_destinationLocation = LatLng(
|
|
latitude: address!.latitude!,
|
|
longitude: address.longitude!,
|
|
);
|
|
});
|
|
// Just store the destination, don't move camera
|
|
// The navigation will handle camera positioning
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _applyDarkModeStyle() async {
|
|
// Check if widget is still mounted and controller exists
|
|
if (!mounted || _navigationController == null) return;
|
|
|
|
try {
|
|
if (!mounted) return;
|
|
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
if (isDarkMode) {
|
|
// Dark mode style - Note: Currently only supported on Android
|
|
const simpleDarkStyle = '''[
|
|
{
|
|
"elementType": "geometry",
|
|
"stylers": [{"color": "#242424"}]
|
|
},
|
|
{
|
|
"elementType": "labels.text.fill",
|
|
"stylers": [{"color": "#746855"}]
|
|
},
|
|
{
|
|
"elementType": "labels.text.stroke",
|
|
"stylers": [{"color": "#242424"}]
|
|
},
|
|
{
|
|
"featureType": "water",
|
|
"elementType": "geometry",
|
|
"stylers": [{"color": "#17263c"}]
|
|
}
|
|
]''';
|
|
|
|
await _navigationController!.setMapStyle(simpleDarkStyle);
|
|
} else {
|
|
// Reset to default light style
|
|
await _navigationController!.setMapStyle(null);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
debugPrint('Error applying map style: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _startNavigation() async {
|
|
if (_destinationLocation == null) return;
|
|
|
|
// Show loading indicator
|
|
if (mounted) {
|
|
setState(() {
|
|
_isStartingNavigation = true;
|
|
_loadingMessage = 'Starting navigation...';
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Ensure session is initialized before starting navigation
|
|
if (!_isSessionInitialized && !_isInitializing) {
|
|
debugPrint('Initializing navigation session...');
|
|
await _initializeNavigation();
|
|
}
|
|
|
|
// Wait for initialization to complete if it's in progress
|
|
int retries = 0;
|
|
while (!_isSessionInitialized && retries < 30) {
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
retries++;
|
|
}
|
|
|
|
if (!_isSessionInitialized) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isStartingNavigation = false;
|
|
});
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Navigation initialization timeout'),
|
|
duration: Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_loadingMessage = 'Setting destination...';
|
|
});
|
|
}
|
|
|
|
final waypoint = NavigationWaypoint.withLatLngTarget(
|
|
title: widget.selectedDelivery?.name ?? 'Destination',
|
|
target: _destinationLocation!,
|
|
);
|
|
|
|
final destinations = Destinations(
|
|
waypoints: [waypoint],
|
|
displayOptions: NavigationDisplayOptions(showDestinationMarkers: true),
|
|
);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_loadingMessage = 'Starting guidance...';
|
|
});
|
|
}
|
|
|
|
debugPrint('Setting destinations: ${_destinationLocation!.latitude}, ${_destinationLocation!.longitude}');
|
|
await GoogleMapsNavigator.setDestinations(destinations);
|
|
|
|
debugPrint('Starting guidance...');
|
|
await GoogleMapsNavigator.startGuidance();
|
|
|
|
debugPrint('Navigation started successfully');
|
|
|
|
// Reapply dark mode style after navigation starts
|
|
if (mounted) {
|
|
await _applyDarkModeStyle();
|
|
}
|
|
|
|
// Auto-recenter on driver location when navigation starts
|
|
await _recenterMap();
|
|
debugPrint('Camera recentered on driver location');
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isNavigating = true;
|
|
_isStartingNavigation = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
final errorMessage = _formatErrorMessage(e);
|
|
debugPrint('Navigation start error: $errorMessage');
|
|
debugPrint('Full error: $e');
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isStartingNavigation = false;
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Navigation error: $errorMessage'),
|
|
duration: const Duration(seconds: 4),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _stopNavigation() async {
|
|
try {
|
|
await GoogleMapsNavigator.stopGuidance();
|
|
await GoogleMapsNavigator.clearDestinations();
|
|
if (mounted) {
|
|
setState(() {
|
|
_isNavigating = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
debugPrint('Navigation stop error: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _zoomIn() async {
|
|
if (_navigationController == null) return;
|
|
try {
|
|
final currentCamera = await _navigationController!.getCameraPosition();
|
|
await _navigationController!.animateCamera(
|
|
CameraUpdate.newLatLngZoom(
|
|
currentCamera.target,
|
|
currentCamera.zoom + 1,
|
|
),
|
|
);
|
|
} catch (e) {
|
|
debugPrint('Zoom in error: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _zoomOut() async {
|
|
if (_navigationController == null) return;
|
|
try {
|
|
final currentCamera = await _navigationController!.getCameraPosition();
|
|
await _navigationController!.animateCamera(
|
|
CameraUpdate.newLatLngZoom(
|
|
currentCamera.target,
|
|
currentCamera.zoom - 1,
|
|
),
|
|
);
|
|
} catch (e) {
|
|
debugPrint('Zoom out error: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _recenterMap() async {
|
|
if (_navigationController == null) return;
|
|
try {
|
|
// Use the navigation controller's follow location feature
|
|
// This tells the navigation to follow the driver's current location
|
|
await _navigationController!.followMyLocation(CameraPerspective.tilted);
|
|
debugPrint('Navigation set to follow driver location');
|
|
} catch (e) {
|
|
debugPrint('Recenter map error: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Driver's current location (defaults to Montreal if not available)
|
|
final initialPosition = const LatLng(latitude: 45.5017, longitude: -73.5673);
|
|
|
|
// Calculate dynamic padding for top info panel and bottom button bar
|
|
// Increased to accommodate navigation widget info and action buttons
|
|
final topPadding = widget.selectedDelivery != null ? 110.0 : 0.0;
|
|
final bottomPadding = widget.selectedDelivery != null ? 110.0 : 0.0;
|
|
|
|
return Stack(
|
|
children: [
|
|
// Map with padding to accommodate overlaid elements
|
|
Padding(
|
|
padding: EdgeInsets.only(
|
|
top: topPadding,
|
|
bottom: bottomPadding,
|
|
),
|
|
child: GoogleMapsNavigationView(
|
|
onViewCreated: (controller) async {
|
|
_navigationController = controller;
|
|
// Apply dark mode style with a small delay to ensure map is ready
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
await _applyDarkModeStyle();
|
|
controller.animateCamera(
|
|
CameraUpdate.newLatLngZoom(initialPosition, 12),
|
|
);
|
|
},
|
|
initialCameraPosition: CameraPosition(
|
|
target: initialPosition,
|
|
zoom: 12,
|
|
),
|
|
),
|
|
),
|
|
// Custom dark-themed controls overlay
|
|
Positioned(
|
|
top: 16,
|
|
right: 16,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Zoom in button
|
|
_buildIconButton(
|
|
icon: Icons.add,
|
|
onPressed: _zoomIn,
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Zoom out button
|
|
_buildIconButton(
|
|
icon: Icons.remove,
|
|
onPressed: _zoomOut,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Navigation and action buttons (positioned above bottom button bar)
|
|
Positioned(
|
|
bottom: 120,
|
|
right: 16,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Start navigation button
|
|
if (widget.selectedDelivery != null && !_isNavigating)
|
|
_buildActionButton(
|
|
label: _isStartingNavigation || _isInitializing ? 'Loading...' : 'Navigate',
|
|
icon: Icons.directions,
|
|
onPressed: _isStartingNavigation || _isInitializing ? null : _startNavigation,
|
|
color: SvrntyColors.crimsonRed,
|
|
),
|
|
if (_isNavigating)
|
|
_buildActionButton(
|
|
label: 'Stop',
|
|
icon: Icons.stop,
|
|
onPressed: _stopNavigation,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Selected delivery info panel (dark themed)
|
|
if (widget.selectedDelivery != null)
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.2),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
widget.selectedDelivery!.name,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.titleMedium
|
|
?.copyWith(fontWeight: FontWeight.w600),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
widget.selectedDelivery!.deliveryAddress
|
|
?.formattedAddress ??
|
|
'No address',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.bodySmall,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Status indicator
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: widget.selectedDelivery!.delivered
|
|
? SvrntyColors.statusCompleted
|
|
: SvrntyColors.statusPending,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
widget.selectedDelivery!.delivered
|
|
? 'Delivered'
|
|
: 'Pending',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.labelSmall
|
|
?.copyWith(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Bottom action button bar
|
|
if (widget.selectedDelivery != null)
|
|
Positioned(
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.2),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Recenter button
|
|
Expanded(
|
|
child: _buildBottomActionButton(
|
|
label: 'Recenter',
|
|
icon: Icons.location_on,
|
|
onPressed: _recenterMap,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
// Mark Complete button (if not already delivered)
|
|
if (!widget.selectedDelivery!.delivered)
|
|
Expanded(
|
|
child: _buildBottomActionButton(
|
|
label: 'Mark Complete',
|
|
icon: Icons.check_circle,
|
|
onPressed: () => widget.onAction?.call('complete'),
|
|
isPrimary: true,
|
|
),
|
|
),
|
|
if (widget.selectedDelivery!.delivered)
|
|
Expanded(
|
|
child: _buildBottomActionButton(
|
|
label: _isInitializing ? 'Initializing...' : 'Start Navigation',
|
|
icon: Icons.directions,
|
|
onPressed: _isInitializing ? null : _startNavigation,
|
|
isPrimary: true,
|
|
),
|
|
),
|
|
if (!_isNavigating && !widget.selectedDelivery!.delivered)
|
|
const SizedBox(width: 12),
|
|
if (!_isNavigating && !widget.selectedDelivery!.delivered)
|
|
Expanded(
|
|
child: _buildBottomActionButton(
|
|
label: _isStartingNavigation || _isInitializing ? 'Loading...' : 'Navigate',
|
|
icon: Icons.directions,
|
|
onPressed: _isStartingNavigation || _isInitializing ? null : _startNavigation,
|
|
),
|
|
),
|
|
if (_isNavigating)
|
|
const SizedBox(width: 12),
|
|
if (_isNavigating)
|
|
Expanded(
|
|
child: _buildBottomActionButton(
|
|
label: 'Stop',
|
|
icon: Icons.stop,
|
|
onPressed: _stopNavigation,
|
|
isDanger: true,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Loading overlay during navigation initialization and start
|
|
if (_isStartingNavigation || _isInitializing)
|
|
Positioned.fill(
|
|
child: Container(
|
|
color: Colors.black.withValues(alpha: 0.4),
|
|
child: Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.3),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Circular progress indicator
|
|
SizedBox(
|
|
width: 60,
|
|
height: 60,
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Theme.of(context).colorScheme.primary,
|
|
),
|
|
strokeWidth: 3,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Loading message
|
|
Text(
|
|
_loadingMessage,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Secondary message
|
|
Text(
|
|
'Please wait...',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildIconButton({
|
|
required IconData icon,
|
|
required VoidCallback onPressed,
|
|
}) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.3),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Material(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
shape: const CircleBorder(),
|
|
child: InkWell(
|
|
onTap: onPressed,
|
|
customBorder: const CircleBorder(),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Icon(
|
|
icon,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
size: 24,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBottomActionButton({
|
|
required String label,
|
|
required IconData icon,
|
|
required VoidCallback? onPressed,
|
|
bool isPrimary = false,
|
|
bool isDanger = false,
|
|
}) {
|
|
Color backgroundColor;
|
|
Color textColor = Colors.white;
|
|
|
|
if (isDanger) {
|
|
backgroundColor = Colors.red.shade600;
|
|
} else if (isPrimary) {
|
|
backgroundColor = SvrntyColors.crimsonRed;
|
|
} else {
|
|
backgroundColor = Theme.of(context).colorScheme.surfaceContainerHighest;
|
|
textColor = Theme.of(context).colorScheme.onSurface;
|
|
}
|
|
|
|
// Reduce opacity when disabled
|
|
if (onPressed == null) {
|
|
backgroundColor = backgroundColor.withValues(alpha: 0.5);
|
|
}
|
|
|
|
return Material(
|
|
color: backgroundColor,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: InkWell(
|
|
onTap: onPressed,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 12,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: textColor,
|
|
size: 18,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActionButton({
|
|
required String label,
|
|
required IconData icon,
|
|
required VoidCallback? onPressed,
|
|
required Color color,
|
|
}) {
|
|
final isDisabled = onPressed == null;
|
|
final buttonColor = isDisabled ? color.withValues(alpha: 0.5) : color;
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.3),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Material(
|
|
color: buttonColor,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: InkWell(
|
|
onTap: onPressed,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: Colors.white,
|
|
size: 18,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|