MotorSignal-App/lib/features/dashboard/widgets/navigation_maneuver_card.dart

251 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/theme/theme_extensions.dart';
import '../../../services/navigation/navigation_session_service.dart';
/// High-contrast, glanceable turn-by-turn navigation HUD card mounted on the Dashboard.
///
/// Disappears when navigation is inactive. When active, displays high-visibility
/// maneuver direction, next road, countdown distance in feet/miles, ETA, and
/// quick mute/end controls.
class NavigationManeuverCard extends ConsumerWidget {
const NavigationManeuverCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final navState = ref.watch(navigationSessionProvider);
if (!navState.isNavigating || navState.currentManeuver == null) {
return const SizedBox.shrink();
}
final currentManeuver = navState.currentManeuver!;
final nextManeuver = navState.nextManeuver;
final colors = context.motorColors;
return Semantics(
label: 'Navigation: ${currentManeuver.instruction}. ${navState.formattedDistanceToNext} to turn. Remaining: ${navState.formattedRemainingDistance}, ETA ${navState.formattedEta}.',
child: Card(
key: const ValueKey('navigation_maneuver_card'),
margin: const EdgeInsets.only(bottom: 12),
elevation: 6,
color: const Color(0xFF0F172A), // Dark tactical navy/slate for extreme contrast
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: colors.cyberCyan.withValues(alpha: 0.6),
width: 1.5,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {
// Tap navigates to live map
context.go('/map');
},
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Row: Big Turn Icon, Distance, Street & Voice/Exit controls
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// High-contrast Turn Direction Icon Badge
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: colors.cyberCyan.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: colors.cyberCyan,
width: 1.8,
),
),
alignment: Alignment.center,
child: Icon(
currentManeuver.type.icon,
size: 32,
color: colors.cyberCyan,
),
),
const SizedBox(width: 14),
// Maneuver Distance and Street Name
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
navState.formattedDistanceToNext,
style: const TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 22,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: -0.5,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: colors.cyberCyan.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
currentManeuver.type.directionShort,
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 10,
fontWeight: FontWeight.w800,
color: colors.cyberCyan,
letterSpacing: 0.8,
),
),
),
],
),
),
const SizedBox(height: 2),
Text(
currentManeuver.streetName ?? currentManeuver.instruction,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
// Quick Audio Mute & End Navigation Buttons
IconButton(
key: const ValueKey('nav_toggle_tts_button'),
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
padding: EdgeInsets.zero,
icon: Icon(
navState.ttsEnabled ? Icons.volume_up : Icons.volume_off,
color: navState.ttsEnabled ? colors.cyberCyan : Colors.white54,
size: 22,
),
tooltip: navState.ttsEnabled ? 'Mute Voice Guidance' : 'Unmute Voice Guidance',
onPressed: () {
ref.read(navigationSessionProvider.notifier).toggleTts();
},
),
IconButton(
key: const ValueKey('nav_stop_button'),
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, color: Colors.white70, size: 22),
tooltip: 'Exit Navigation',
onPressed: () {
ref.read(navigationSessionProvider.notifier).stopNavigation();
},
),
],
),
const SizedBox(height: 10),
Divider(
height: 1,
color: colors.outlineVariant.withValues(alpha: 0.4),
),
const SizedBox(height: 8),
Wrap(
spacing: 12,
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.spaceBetween,
children: [
Wrap(
spacing: 8,
runSpacing: 2,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.schedule, size: 14, color: colors.cyberCyan),
const SizedBox(width: 4),
Text(
'${navState.formattedDurationRemaining} (${navState.formattedEta})',
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
fontWeight: FontWeight.bold,
color: colors.cyberCyan,
),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.route, size: 14, color: Colors.white70),
const SizedBox(width: 4),
Text(
navState.formattedRemainingDistance,
style: const TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.white70,
),
),
],
),
],
),
if (nextManeuver != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Then',
style: TextStyle(
fontSize: 10.5,
color: Colors.white54,
fontFamily: 'Inter',
),
),
const SizedBox(width: 4),
Icon(
nextManeuver.type.icon,
size: 14,
color: colors.cyberCyan,
),
],
),
],
),
],
),
),
),
),
);
}
}