251 lines
11 KiB
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,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|