chore: add build artifacts, map assets, and routing data
This commit is contained in:
@@ -12,93 +12,74 @@ class ActiveNavigationTopBanner extends StatelessWidget {
|
||||
final step = navState.currentStep;
|
||||
if (step == null) return const SizedBox.shrink();
|
||||
|
||||
return Positioned(
|
||||
top: 50,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF50F172A),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF0071E3), width: 1.5),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black87, blurRadius: 20, offset: Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Maneuver Icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0071E3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0x660071E3), blurRadius: 8),
|
||||
],
|
||||
),
|
||||
child: Icon(step.icon, color: Colors.white, size: 28),
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF50F172A),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF0071E3), width: 1.5),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black87, blurRadius: 20, offset: Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Maneuver Icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0071E3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0x660071E3), blurRadius: 8),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
child: Icon(step.icon, color: Colors.white, size: 28),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// Instruction Text
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'بعد ${step.distanceMeters.round()} متر',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF38BDF8),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
// Instruction Text
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'بعد ${step.distanceMeters.round()} متر',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF38BDF8),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
step.instructionAr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (step.streetName.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
step.instructionAr,
|
||||
step.streetName,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF94A3B8),
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Speed badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF020617),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
navState.currentSpeedKmH.round().toString(),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF4ADE80),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'كم/س',
|
||||
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 9),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -121,60 +102,55 @@ class ActiveNavigationBottomHUD extends StatelessWidget {
|
||||
final hourStr = arrivalTime.hour.toString().padLeft(2, '0');
|
||||
final minStr = arrivalTime.minute.toString().padLeft(2, '0');
|
||||
|
||||
return Positioned(
|
||||
bottom: 20,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF50F172A),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFF22C55E), width: 1.5),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black87, blurRadius: 25, offset: Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Metrics Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildHUDCol('وقت الوصول (ETA)', '$hourStr:$minStr', const Color(0xFF4ADE80)),
|
||||
Container(width: 1, height: 32, color: Colors.white12),
|
||||
_buildHUDCol('المسافة المتبقية', '${navState.remainingDistanceKm} كم', Colors.white),
|
||||
Container(width: 1, height: 32, color: Colors.white12),
|
||||
_buildHUDCol('الزمن المتبقي', '${navState.remainingDurationMinutes.round()} دقيقة', const Color(0xFF38BDF8)),
|
||||
],
|
||||
),
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF50F172A),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFF22C55E), width: 1.5),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black87, blurRadius: 25, offset: Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Metrics Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildHUDCol('وقت الوصول (ETA)', '$hourStr:$minStr', const Color(0xFF4ADE80)),
|
||||
Container(width: 1, height: 32, color: Colors.white12),
|
||||
_buildHUDCol('المسافة المتبقية', '${navState.remainingDistanceKm} كم', Colors.white),
|
||||
Container(width: 1, height: 32, color: Colors.white12),
|
||||
_buildHUDCol('الزمن المتبقي', '${navState.remainingDurationMinutes.round()} دقيقة', const Color(0xFF38BDF8)),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Stop Navigation Button
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onStopNavigation,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
label: const Text(
|
||||
'إنهاء الملاحة الميدانية (Exit Navigation)',
|
||||
style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFEF4444),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
// Stop Navigation Button
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onStopNavigation,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
label: const Text(
|
||||
'إنهاء الملاحة الميدانية (Exit Navigation)',
|
||||
style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFEF4444),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_compass/flutter_compass.dart';
|
||||
import 'package:sensors_plus/sensors_plus.dart';
|
||||
import '../models/angle_unit.dart';
|
||||
import '../models/landmark.dart';
|
||||
import '../services/landmark_database.dart';
|
||||
@@ -15,7 +18,9 @@ class CameraResectionView extends StatefulWidget {
|
||||
final Function(ResectionObservation) onObservationAdded;
|
||||
final VoidCallback onCalculatePressed;
|
||||
final VoidCallback onResetPressed;
|
||||
final VoidCallback? onPickFromMap;
|
||||
final VoidCallback onClose;
|
||||
final TacticalLandmark? initialSelectedLandmark;
|
||||
|
||||
const CameraResectionView({
|
||||
super.key,
|
||||
@@ -24,7 +29,9 @@ class CameraResectionView extends StatefulWidget {
|
||||
required this.onObservationAdded,
|
||||
required this.onCalculatePressed,
|
||||
required this.onResetPressed,
|
||||
this.onPickFromMap,
|
||||
required this.onClose,
|
||||
this.initialSelectedLandmark,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -36,6 +43,20 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
bool _isCameraReady = false;
|
||||
double _headingDeg = 0.0;
|
||||
bool _isLockedAnim = false;
|
||||
double _currentZoom = 1.0;
|
||||
double _minZoom = 1.0;
|
||||
double _maxZoom = 8.0;
|
||||
double _baseScale = 1.0;
|
||||
|
||||
// ── Single Landmark 15m Mode State ─────────────────────────────────────────
|
||||
bool _isSingleLandmarkMode = false;
|
||||
int _singlePhase = 0; // 0: Aim 1, 1: Walk 15m, 2: Aim 2
|
||||
double _firstAzimuth = 0.0;
|
||||
double _secondAzimuth = 0.0;
|
||||
double _walkedDistanceMeters = 0.0;
|
||||
int _stepCount = 0;
|
||||
dynamic _accelSubscription;
|
||||
DateTime _lastStepTime = DateTime.now();
|
||||
|
||||
// Search state
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
@@ -52,6 +73,11 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
}
|
||||
|
||||
void _initDefaultLandmark() {
|
||||
if (widget.initialSelectedLandmark != null) {
|
||||
_selectedLandmark = widget.initialSelectedLandmark;
|
||||
_searchResults = [widget.initialSelectedLandmark!, ...LandmarkDatabase.allLandmarks.take(9)];
|
||||
return;
|
||||
}
|
||||
final all = LandmarkDatabase.allLandmarks;
|
||||
if (all.isNotEmpty) {
|
||||
_selectedLandmark = all[0];
|
||||
@@ -69,6 +95,11 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
enableAudio: false,
|
||||
);
|
||||
await _cameraController!.initialize();
|
||||
|
||||
_minZoom = await _cameraController!.getMinZoomLevel();
|
||||
_maxZoom = math.min(10.0, await _cameraController!.getMaxZoomLevel());
|
||||
_currentZoom = _minZoom;
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isCameraReady = true);
|
||||
}
|
||||
@@ -78,18 +109,55 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setZoom(double zoom) async {
|
||||
final clamped = zoom.clamp(_minZoom, _maxZoom);
|
||||
if (clamped != _currentZoom) {
|
||||
setState(() => _currentZoom = clamped);
|
||||
try {
|
||||
await _cameraController?.setZoomLevel(clamped);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
bool _isFrameFrozen = false;
|
||||
|
||||
void _initCompass() {
|
||||
FlutterCompass.events?.listen((event) {
|
||||
if (event.heading != null && mounted) {
|
||||
if (_isFrameFrozen) return; // Keep heading locked while frame is frozen
|
||||
final raw = (event.heading! + 360) % 360;
|
||||
final diff = ((raw - _headingDeg + 180) % 360) - 180;
|
||||
final smoothed = (_headingDeg + diff * 0.35 + 360) % 360;
|
||||
setState(() {
|
||||
_headingDeg = (event.heading! + 360) % 360;
|
||||
_headingDeg = smoothed;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _toggleFreezeFrame() async {
|
||||
if (_isFrameFrozen) {
|
||||
try {
|
||||
await _cameraController?.resumePreview();
|
||||
} catch (_) {}
|
||||
if (mounted) {
|
||||
setState(() => _isFrameFrozen = false);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await _cameraController?.pausePreview();
|
||||
if (mounted) {
|
||||
setState(() => _isFrameFrozen = true);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Freeze frame pause error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_accelSubscription?.cancel();
|
||||
_cameraController?.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
@@ -107,16 +175,13 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
|
||||
setState(() => _isSearching = true);
|
||||
|
||||
// 1. Search SQLite Local Places Database (Mosques, Schools, Intersections, Hills)
|
||||
List<TacticalLandmark> dbResults = [];
|
||||
try {
|
||||
dbResults = await LocalSqliteDb.searchPlaces(cleanQuery, limit: 30);
|
||||
} catch (_) {}
|
||||
|
||||
// 2. Search Static Strategic Landmarks
|
||||
final staticResults = LandmarkDatabase.search(cleanQuery);
|
||||
|
||||
// 3. Search Online Geocoding API if local results are few
|
||||
List<TacticalLandmark> onlineResults = [];
|
||||
if (dbResults.length < 5) {
|
||||
try {
|
||||
@@ -144,7 +209,78 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
}
|
||||
}
|
||||
|
||||
void _startPedometer() {
|
||||
_stepCount = 0;
|
||||
_walkedDistanceMeters = 0.0;
|
||||
_accelSubscription?.cancel();
|
||||
|
||||
_accelSubscription = accelerometerEventStream().listen((event) {
|
||||
final mag = math.sqrt(event.x * event.x + event.y * event.y + event.z * event.z);
|
||||
final now = DateTime.now();
|
||||
if (mag > 11.5 && now.difference(_lastStepTime).inMilliseconds > 300) {
|
||||
_lastStepTime = now;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_stepCount++;
|
||||
_walkedDistanceMeters = math.min(15.0, _stepCount * 0.75);
|
||||
if (_walkedDistanceMeters >= 15.0 && _singlePhase == 1) {
|
||||
_singlePhase = 2;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _lockSingleLandmarkPhase() {
|
||||
if (_selectedLandmark == null) return;
|
||||
|
||||
if (_singlePhase == 0) {
|
||||
_firstAzimuth = _headingDeg;
|
||||
_singlePhase = 1;
|
||||
_startPedometer();
|
||||
setState(() => _isLockedAnim = true);
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (mounted) setState(() => _isLockedAnim = false);
|
||||
});
|
||||
} else if (_singlePhase == 1) {
|
||||
_singlePhase = 2;
|
||||
_accelSubscription?.cancel();
|
||||
setState(() {});
|
||||
} else if (_singlePhase == 2) {
|
||||
_secondAzimuth = _headingDeg;
|
||||
_accelSubscription?.cancel();
|
||||
|
||||
final obs = ResectionObservation(
|
||||
landmark: _selectedLandmark!,
|
||||
observedAzimuthDeg: _firstAzimuth,
|
||||
trueAzimuthDeg: ResectionCalculator.getTrueAzimuth(_firstAzimuth),
|
||||
);
|
||||
|
||||
final obs2 = ResectionObservation(
|
||||
landmark: _selectedLandmark!,
|
||||
observedAzimuthDeg: _secondAzimuth,
|
||||
trueAzimuthDeg: ResectionCalculator.getTrueAzimuth(_secondAzimuth),
|
||||
);
|
||||
|
||||
widget.onObservationAdded(obs);
|
||||
widget.onObservationAdded(obs2);
|
||||
|
||||
setState(() => _isLockedAnim = true);
|
||||
Future.delayed(const Duration(milliseconds: 400), () {
|
||||
if (mounted) {
|
||||
widget.onCalculatePressed();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _lockCurrentObservation() {
|
||||
if (_isSingleLandmarkMode) {
|
||||
_lockSingleLandmarkPhase();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedLandmark == null) return;
|
||||
|
||||
final trueAzimuth = ResectionCalculator.getTrueAzimuth(_headingDeg);
|
||||
@@ -194,15 +330,19 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
// ── 1. Camera Feed / Viewfinder Background ─────────────────
|
||||
// ── 1. Camera Feed / Viewfinder Background with Pinch Zoom ─
|
||||
if (_isCameraReady && _cameraController != null)
|
||||
SizedBox.expand(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: _cameraController!.value.previewSize?.height ?? 1,
|
||||
height: _cameraController!.value.previewSize?.width ?? 1,
|
||||
child: CameraPreview(_cameraController!),
|
||||
GestureDetector(
|
||||
onScaleStart: (_) => _baseScale = _currentZoom,
|
||||
onScaleUpdate: (details) => _setZoom(_baseScale * details.scale),
|
||||
child: SizedBox.expand(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: _cameraController!.value.previewSize?.height ?? 1,
|
||||
height: _cameraController!.value.previewSize?.width ?? 1,
|
||||
child: CameraPreview(_cameraController!),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -220,6 +360,111 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
),
|
||||
),
|
||||
|
||||
// ── Controls on Right Side (Freeze Frame & Zoom Slider) ───
|
||||
if (_isCameraReady)
|
||||
Positioned(
|
||||
right: 14,
|
||||
top: 210,
|
||||
bottom: 230,
|
||||
child: Column(
|
||||
children: [
|
||||
// Freeze Frame Button
|
||||
GestureDetector(
|
||||
onTap: _toggleFreezeFrame,
|
||||
child: Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: _isFrameFrozen ? const Color(0xFFEF4444) : const Color(0xFF090E17).withAlpha(200),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: _isFrameFrozen ? const Color(0xFFFF7171) : const Color(0xFF00F0FF),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
if (_isFrameFrozen)
|
||||
const BoxShadow(color: Color(0x99EF4444), blurRadius: 10),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
_isFrameFrozen ? Icons.pause : Icons.camera_alt_outlined,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Zoom Slider Container
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 38,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF090E17).withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('8x', style: TextStyle(color: Colors.white54, fontSize: 8.5)),
|
||||
Expanded(
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackHeight: 2.5,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
|
||||
activeTrackColor: const Color(0xFF00F0FF),
|
||||
inactiveTrackColor: Colors.white24,
|
||||
thumbColor: Colors.white,
|
||||
),
|
||||
child: Slider(
|
||||
value: _currentZoom,
|
||||
min: _minZoom,
|
||||
max: _maxZoom,
|
||||
onChanged: (val) => _setZoom(val),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('1x', style: TextStyle(color: Colors.white54, fontSize: 8.5)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── Frozen Frame Indicator Banner ─────────────────────────
|
||||
if (_isFrameFrozen)
|
||||
Positioned(
|
||||
top: 155,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xEEEF4444),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: const [BoxShadow(color: Color(0x66EF4444), blurRadius: 10)],
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.ac_unit, color: Colors.white, size: 14),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'تم تجميد الصورة لمنع الاهتزاز — سدد واقفل الرصد',
|
||||
style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── 2. Tactical Military Crosshair & Reticle ────────────────
|
||||
TacticalCrosshair(
|
||||
headingDeg: _headingDeg,
|
||||
@@ -243,32 +488,46 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
children: [
|
||||
// Step Indicator Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF00F172A),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: obsCount >= 2 ? const Color(0xFF22C55E) : const Color(0xFF0071E3),
|
||||
color: (_isSingleLandmarkMode ? _singlePhase >= 2 : obsCount >= 2)
|
||||
? const Color(0xFF22C55E)
|
||||
: const Color(0xFF0071E3),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
obsCount >= 2 ? Icons.check_circle : Icons.gps_fixed,
|
||||
size: 16,
|
||||
color: obsCount >= 2 ? const Color(0xFF4ADE80) : const Color(0xFF38BDF8),
|
||||
(_isSingleLandmarkMode ? _singlePhase >= 2 : obsCount >= 2)
|
||||
? Icons.check_circle
|
||||
: Icons.gps_fixed,
|
||||
size: 15,
|
||||
color: (_isSingleLandmarkMode ? _singlePhase >= 2 : obsCount >= 2)
|
||||
? const Color(0xFF4ADE80)
|
||||
: const Color(0xFF38BDF8),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
obsCount == 0
|
||||
? 'الخطوة 1: ابحث وسدد على المعلم الأول'
|
||||
: (obsCount == 1
|
||||
? 'الخطوة 2: ابحث وسدد على المعلم الثاني'
|
||||
: 'تم رصد $obsCount معالم • جاري استخراج الموقع'),
|
||||
_isSingleLandmarkMode
|
||||
? (_singlePhase == 0
|
||||
? '1. سدد على المعلم واقفل الزاوية الأولى'
|
||||
: (_singlePhase == 1
|
||||
? '2. تحرك 15 متراً عمودياً على خط النظر'
|
||||
: '3. سدد مرة أخرى واقفل الزاوية الثانية'))
|
||||
: (obsCount == 0
|
||||
? '1. ابحث وسدد على المعلم الأول'
|
||||
: (obsCount == 1
|
||||
? '2. ابحث وسدد على المعلم الثاني'
|
||||
: 'تم رصد $obsCount معالم • جاري الحساب')),
|
||||
style: TextStyle(
|
||||
color: obsCount >= 2 ? const Color(0xFF4ADE80) : Colors.white,
|
||||
fontSize: 11.5,
|
||||
color: (_isSingleLandmarkMode ? _singlePhase >= 2 : obsCount >= 2)
|
||||
? const Color(0xFF4ADE80)
|
||||
: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -288,34 +547,147 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Mode Selector Toggle (رصد متعدد vs معلم واحد + 15م بالحساسات)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xCC0F172A),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isSingleLandmarkMode = false;
|
||||
_singlePhase = 0;
|
||||
_accelSubscription?.cancel();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: !_isSingleLandmarkMode ? const Color(0xFF0071E3) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'🎯 تقاطع متعدد (2-3 معالم)',
|
||||
style: TextStyle(
|
||||
color: !_isSingleLandmarkMode ? Colors.white : const Color(0xFF94A3B8),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isSingleLandmarkMode = true;
|
||||
_singlePhase = 0;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: _isSingleLandmarkMode ? const Color(0xFF10B981) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'📏 معلم واحد (خط أساس 15م)',
|
||||
style: TextStyle(
|
||||
color: _isSingleLandmarkMode ? Colors.white : const Color(0xFF94A3B8),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Interactive Map Picker Trigger Button
|
||||
if (widget.onPickFromMap != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: widget.onPickFromMap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF7C3AED), Color(0xFF4F46E5)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFA78BFA), width: 1.2),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x667C3AED),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.touch_app, color: Colors.white, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'📍 تأشير واختيار المعلم مباشرة من الخريطة',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Unified Place / Mosque / Landmark Search Field
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF50F172A),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFF0071E3)),
|
||||
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 15)],
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: _onSearchChanged,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'ابحث عن أي مسجد، مبنى، قلعة، تقاطع، أو معلم...',
|
||||
hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
||||
prefixIcon: const Icon(Icons.search, color: Color(0xFF38BDF8), size: 20),
|
||||
hintText: 'ابحث عن معلم، مئذنة، مدرسة، قمة جبل...',
|
||||
hintStyle: const TextStyle(color: Color(0xFF64748B), fontSize: 12),
|
||||
prefixIcon: const Icon(Icons.search, color: Color(0xFF38BDF8), size: 18),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, size: 16, color: Colors.white70),
|
||||
icon: const Icon(Icons.clear, color: Color(0xFF94A3B8), size: 16),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_onSearchChanged('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 10),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
@@ -385,16 +757,74 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF50F172A),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFF0071E3), width: 1.5),
|
||||
border: Border.all(
|
||||
color: _isSingleLandmarkMode ? const Color(0xFF10B981) : const Color(0xFF0071E3),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: const [BoxShadow(color: Colors.black87, blurRadius: 25)],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Live Pedometer & Sensor 15m Guidance Card (When in Single Landmark Phase 1)
|
||||
if (_isSingleLandmarkMode && _singlePhase == 1)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0x3310B981), Color(0x22059669)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFF10B981), width: 1.2),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.directions_walk, color: Color(0xFF34D399), size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'تحرك 15 متراً عمودياً على خط النظر ($_stepCount خطوة)',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'${_walkedDistanceMeters.toStringAsFixed(1)}م / 15.0م',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF34D399),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: LinearProgressIndicator(
|
||||
value: (_walkedDistanceMeters / 15.0).clamp(0.0, 1.0),
|
||||
minHeight: 8,
|
||||
backgroundColor: Colors.white12,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF10B981)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Targeted Landmark Card
|
||||
if (_selectedLandmark != null)
|
||||
Container(
|
||||
@@ -455,28 +885,49 @@ class _CameraResectionViewState extends State<CameraResectionView> {
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _selectedLandmark != null ? _lockCurrentObservation : null,
|
||||
icon: Icon(
|
||||
obsCount >= 1 ? Icons.my_location : Icons.lock_outline,
|
||||
_isSingleLandmarkMode
|
||||
? (_singlePhase == 1 ? Icons.check : (_singlePhase == 2 ? Icons.my_location : Icons.lock_outline))
|
||||
: (obsCount >= 1 ? Icons.my_location : Icons.lock_outline),
|
||||
size: 18,
|
||||
),
|
||||
label: Text(
|
||||
obsCount == 0
|
||||
? 'تثبيت رصد المعلم الأول'
|
||||
: 'تثبيت رصد المعلم الثاني (استخراج الموقع فوراً)',
|
||||
style: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w900),
|
||||
_isSingleLandmarkMode
|
||||
? (_singlePhase == 0
|
||||
? 'تثبيت الرصد الأول وبدء خط الأساس 15م'
|
||||
: (_singlePhase == 1
|
||||
? 'وصلت 15 متراً (انتقل لتسديد الرصد الثاني)'
|
||||
: 'تثبيت الرصد الثاني (حساب المسافة والموقع فوراً)'))
|
||||
: (obsCount == 0
|
||||
? 'تثبيت رصد المعلم الأول'
|
||||
: 'تثبيت رصد المعلم الثاني (استخراج الموقع فوراً)'),
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w900),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: obsCount >= 1 ? const Color(0xFF22C55E) : const Color(0xFF0071E3),
|
||||
backgroundColor: _isSingleLandmarkMode
|
||||
? (_singlePhase == 1 ? const Color(0xFFF59E0B) : const Color(0xFF10B981))
|
||||
: (obsCount >= 1 ? const Color(0xFF22C55E) : const Color(0xFF0071E3)),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (obsCount > 0) ...[
|
||||
if (obsCount > 0 || (_isSingleLandmarkMode && _singlePhase > 0)) ...[
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filledTonal(
|
||||
onPressed: widget.onResetPressed,
|
||||
onPressed: () {
|
||||
if (_isSingleLandmarkMode) {
|
||||
setState(() {
|
||||
_singlePhase = 0;
|
||||
_stepCount = 0;
|
||||
_walkedDistanceMeters = 0.0;
|
||||
_accelSubscription?.cancel();
|
||||
});
|
||||
} else {
|
||||
widget.onResetPressed();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0x33EF4444),
|
||||
|
||||
@@ -14,6 +14,7 @@ enum MapPickerTarget {
|
||||
minefieldStart,
|
||||
minefieldEnd,
|
||||
isochroneCenter,
|
||||
resectionLandmark,
|
||||
}
|
||||
|
||||
class InteractiveMapPickerHud extends StatelessWidget {
|
||||
@@ -54,6 +55,8 @@ class InteractiveMapPickerHud extends StatelessWidget {
|
||||
return 'تحديد نهاية حقل الألغام (Point B) ⚠️';
|
||||
case MapPickerTarget.isochroneCenter:
|
||||
return 'تحديد قاعدة قوة التدخل السريع (QRF) ⚡';
|
||||
case MapPickerTarget.resectionLandmark:
|
||||
return 'تحديد المعلم الجغرافي للتقاطع البصري (Landmark) 📍';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +78,7 @@ class InteractiveMapPickerHud extends StatelessWidget {
|
||||
case MapPickerTarget.artilleryGun:
|
||||
return const Color(0xFF0071E3);
|
||||
case MapPickerTarget.isochroneCenter:
|
||||
case MapPickerTarget.resectionLandmark:
|
||||
return const Color(0xFFA855F7);
|
||||
}
|
||||
}
|
||||
@@ -100,7 +104,9 @@ class InteractiveMapPickerHud extends StatelessWidget {
|
||||
case MapPickerTarget.minefieldEnd:
|
||||
return Icons.warning_amber;
|
||||
case MapPickerTarget.isochroneCenter:
|
||||
return Icons.timelapse;
|
||||
return Icons.flash_on;
|
||||
case MapPickerTarget.resectionLandmark:
|
||||
return Icons.location_searching;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,6 @@ class _OfflinePackageDialogState extends State<OfflinePackageDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canSync = _info?.canSyncNow ?? true;
|
||||
final remaining = _info?.daysUntilNextSync ?? 0;
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: const Color(0xFF0F172A),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: const BorderSide(color: Color(0xFF0071E3), width: 1.5)),
|
||||
@@ -102,9 +99,9 @@ class _OfflinePackageDialogState extends State<OfflinePackageDialog> {
|
||||
const Divider(color: Colors.white12, height: 14),
|
||||
_buildStatRow('حجم معالم PostGIS المفهرسة:', '~25 MB (SQLite FTS5 سريع)'),
|
||||
const Divider(color: Colors.white12, height: 14),
|
||||
_buildStatRow('محرك التوجيه غير المتصل:', '100% On-Device (خوارزمية A*)'),
|
||||
_buildStatRow('محرك التوجيه غير المتصل:', '100% On-Device (محرك Valhalla الحقيقي)'),
|
||||
const Divider(color: Colors.white12, height: 14),
|
||||
_buildStatRow('قفل المزامنة الميداني:', 'كل 14 يوماً لحماية السيرفر'),
|
||||
_buildStatRow('تحديث الحزم الميدانية:', 'متاح دائماً وفوري عند الطلب'),
|
||||
if (_info?.lastSyncTime != null) ...[
|
||||
const Divider(color: Colors.white12, height: 14),
|
||||
_buildStatRow(
|
||||
@@ -161,27 +158,23 @@ class _OfflinePackageDialogState extends State<OfflinePackageDialog> {
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isDownloading
|
||||
? null
|
||||
: (canSync
|
||||
? () => _startSync()
|
||||
: () => _startSync(force: true)),
|
||||
: () => _startSync(force: true),
|
||||
icon: _isDownloading
|
||||
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: Icon(canSync ? Icons.sync : Icons.lock_clock, size: 16),
|
||||
: const Icon(Icons.cloud_download, size: 16),
|
||||
label: Text(
|
||||
_isDownloading
|
||||
? 'جاري المزامنة...'
|
||||
: (canSync
|
||||
? 'مزامنة وتحديث الحزمة الآن'
|
||||
: 'الحزمة محدثة (تحديث إجباري / متبقي $remaining يوم)'),
|
||||
? 'جاري التنزيل والمزامنة...'
|
||||
: 'مزامنة وتنزيل الحزمة الآن (غراف الطرق الحقيقي + المعالم)',
|
||||
style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: canSync ? const Color(0xFF0071E3) : const Color(0xFF1E293B),
|
||||
backgroundColor: const Color(0xFF0071E3),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: canSync ? const Color(0xFF0071E3) : Colors.white24),
|
||||
side: const BorderSide(color: Color(0xFF0071E3)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,780 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/optical_rangefinder_controller.dart';
|
||||
import '../models/angle_unit.dart';
|
||||
import '../services/camera_sensor_calibration_service.dart';
|
||||
|
||||
/// ============================================================================
|
||||
/// [OpticalRangefinderHud] - واجهة قياس المسافة البصري وشبكة التصويب التكتيكية
|
||||
/// ============================================================================
|
||||
class OpticalRangefinderHud extends StatelessWidget {
|
||||
final AngleUnit angleUnit;
|
||||
final VoidCallback onClose;
|
||||
final Function(double distanceMeters, double headingDeg, double pitchDeg)? onRangeLocked;
|
||||
|
||||
const OpticalRangefinderHud({
|
||||
super.key,
|
||||
required this.angleUnit,
|
||||
required this.onClose,
|
||||
this.onRangeLocked,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final OpticalRangefinderController ctrl = Get.put(OpticalRangefinderController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
ctrl.updateScreenDimensions(Size(constraints.maxWidth, constraints.maxHeight));
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// ── 1. Camera Live Stream with Pinch-to-Zoom ─────────────
|
||||
Obx(() {
|
||||
if (!ctrl.isCameraReady.value || ctrl.cameraController == null) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(color: Color(0xFF00F0FF)),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'جاري فحص وتهيئة مستشعر الكاميرا البصري...',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double baseScale = 1.0;
|
||||
|
||||
return GestureDetector(
|
||||
onScaleStart: (_) {
|
||||
baseScale = ctrl.currentZoom.value;
|
||||
},
|
||||
onScaleUpdate: (details) {
|
||||
final newZoom = baseScale * details.scale;
|
||||
ctrl.setZoom(newZoom);
|
||||
},
|
||||
child: Center(
|
||||
child: CameraPreview(ctrl.cameraController!),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// ── 2. Tactical Reticule Overlay (شبكة التصويب الميدانية) ─
|
||||
Obx(() {
|
||||
return _buildTacticalReticule(context, ctrl, constraints.maxWidth, constraints.maxHeight);
|
||||
}),
|
||||
|
||||
// ── 3. Top Operations Bar ────────────────────────────────
|
||||
Positioned(
|
||||
top: 44,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Close Button
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF090E17).withAlpha(220),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: onClose,
|
||||
tooltip: 'إغلاق والعودة للخريطة',
|
||||
),
|
||||
),
|
||||
|
||||
// Title Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF090E17).withAlpha(220),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF00F0FF).withAlpha(120)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.center_focus_strong, color: Color(0xFF00F0FF), size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'قياس المسافة البصري (بدون GPS)',
|
||||
style: TextStyle(color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Sensor Inspector Button
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF090E17).withAlpha(220),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.sensors, color: Color(0xFF38BDF8)),
|
||||
onPressed: () => _openSensorInspectorDialog(context, ctrl),
|
||||
tooltip: 'فحص ومعايرة مستشعر الكاميرا',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── 4. Main Telemetry HUD Card ───────────────────────────
|
||||
Positioned(
|
||||
top: 104,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Obx(() {
|
||||
return _buildMainTelemetryCard(ctrl);
|
||||
}),
|
||||
),
|
||||
|
||||
// ── 5. Right Zoom Slider Widget ──────────────────────────
|
||||
Positioned(
|
||||
right: 14,
|
||||
top: 230,
|
||||
bottom: 210,
|
||||
child: Obx(() {
|
||||
return _buildVerticalZoomBar(ctrl);
|
||||
}),
|
||||
),
|
||||
|
||||
// ── 6. Bottom Target Selector & Actions ──────────────────
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
left: 14,
|
||||
right: 14,
|
||||
child: Obx(() {
|
||||
return _buildBottomControls(context, ctrl);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Reticule Drawing ───────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildTacticalReticule(
|
||||
BuildContext context,
|
||||
OpticalRangefinderController ctrl,
|
||||
double width,
|
||||
double height,
|
||||
) {
|
||||
final centerY = height / 2.0;
|
||||
final centerX = width / 2.0;
|
||||
final bracketH = (ctrl.reticuleHeightRatio.value * height).clamp(20.0, height * 0.75);
|
||||
final halfH = bracketH / 2.0;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// Center crosshair with mil marks
|
||||
CustomPaint(
|
||||
size: Size(width, height),
|
||||
painter: _MilReticulePainter(
|
||||
centerX: centerX,
|
||||
centerY: centerY,
|
||||
bracketHalfHeight: halfH,
|
||||
bracketWidth: 120.0,
|
||||
rollDeg: ctrl.rollDeg.value,
|
||||
),
|
||||
),
|
||||
|
||||
// Interactive Drag Handles for Brackets
|
||||
Positioned(
|
||||
top: centerY - halfH - 24,
|
||||
left: centerX - 60,
|
||||
width: 120,
|
||||
height: 48,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onVerticalDragUpdate: (details) {
|
||||
final newHalf = halfH - details.delta.dy;
|
||||
ctrl.setReticuleHeightRatio((newHalf * 2.0) / height);
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00F0FF).withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Text('قمة الهدف ▲', style: TextStyle(color: Colors.black, fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: centerY + halfH - 24,
|
||||
left: centerX - 60,
|
||||
width: 120,
|
||||
height: 48,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onVerticalDragUpdate: (details) {
|
||||
final newHalf = halfH + details.delta.dy;
|
||||
ctrl.setReticuleHeightRatio((newHalf * 2.0) / height);
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00F0FF).withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Text('قاعدة الهدف ▼', style: TextStyle(color: Colors.black, fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Telemetry Card ─────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildMainTelemetryCard(OpticalRangefinderController ctrl) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF090E17).withAlpha(235),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF00F0FF).withAlpha(150), width: 1.2),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0x66000000), blurRadius: 16),
|
||||
BoxShadow(color: Color(0x3300F0FF), blurRadius: 8),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Distance Display
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
const Text(
|
||||
'المسافة البصرية:',
|
||||
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
ctrl.formatDistance(),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF00F0FF),
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
const Divider(color: Colors.white12, height: 1),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Heading & Pitch Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildMetricChip(
|
||||
icon: Icons.navigation,
|
||||
label: 'الاتجاه:',
|
||||
value: ctrl.formatHeading(angleUnit),
|
||||
color: const Color(0xFF38BDF8),
|
||||
),
|
||||
_buildMetricChip(
|
||||
icon: Icons.height,
|
||||
label: 'الميل:',
|
||||
value: '${ctrl.pitchDeg.value.toStringAsFixed(1)}°',
|
||||
color: const Color(0xFFFBBF24),
|
||||
),
|
||||
_buildMetricChip(
|
||||
icon: Icons.zoom_in,
|
||||
label: 'التقريب:',
|
||||
value: '${ctrl.currentZoom.value.toStringAsFixed(1)}x',
|
||||
color: const Color(0xFF4ADE80),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricChip({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
required Color color,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 13, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: const TextStyle(color: Color(0xFF64748B), fontSize: 10.5)),
|
||||
const SizedBox(width: 4),
|
||||
Text(value, style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Vertical Zoom Bar ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildVerticalZoomBar(OpticalRangefinderController ctrl) {
|
||||
return Container(
|
||||
width: 42,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF090E17).withAlpha(220),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('10x', style: TextStyle(color: Colors.white54, fontSize: 9)),
|
||||
Expanded(
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackHeight: 3,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
|
||||
activeTrackColor: const Color(0xFF00F0FF),
|
||||
inactiveTrackColor: Colors.white24,
|
||||
thumbColor: Colors.white,
|
||||
),
|
||||
child: Slider(
|
||||
value: ctrl.currentZoom.value,
|
||||
min: ctrl.minZoom.value,
|
||||
max: ctrl.maxZoom.value,
|
||||
onChanged: (val) => ctrl.setZoom(val),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('1x', style: TextStyle(color: Colors.white54, fontSize: 9)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Bottom Controls & Target Selector ──────────────────────────────────────
|
||||
|
||||
Widget _buildBottomControls(BuildContext context, OpticalRangefinderController ctrl) {
|
||||
final currentTarget = ctrl.selectedTarget.value;
|
||||
final isCustom = ctrl.isCustomTarget.value;
|
||||
final heightText = isCustom
|
||||
? '${ctrl.customHeight.value.toStringAsFixed(1)} م'
|
||||
: '${currentTarget.heightMeters.toStringAsFixed(1)} م';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF090E17).withAlpha(240),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Target Selector Header Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => _openTargetPickerSheet(context, ctrl),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF020617),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFF0071E3).withAlpha(150)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.tune, color: Color(0xFF38BDF8), size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('الهدف المرجعي المعتمد:', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5)),
|
||||
Text(
|
||||
isCustom ? 'ارتفاع مخصص ($heightText)' : '${currentTarget.titleAr} ($heightText)',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.white70),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
// Quick Lock Button
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0071E3),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () {
|
||||
if (onRangeLocked != null) {
|
||||
onRangeLocked!(
|
||||
ctrl.calculatedDistanceMeters.value,
|
||||
ctrl.headingDeg.value,
|
||||
ctrl.pitchDeg.value,
|
||||
);
|
||||
}
|
||||
Get.snackbar(
|
||||
'🎯 تم تثبيت المدى والاتجاه',
|
||||
'المسافة: ${ctrl.formatDistance()} • ${ctrl.formatHeading(angleUnit)}',
|
||||
backgroundColor: const Color(0xFF0F172A),
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.lock, size: 14),
|
||||
label: const Text('تثبيت المدى', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Quick Presets Row
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[0]), // Infantry
|
||||
const SizedBox(width: 6),
|
||||
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[1]), // Light Vehicle
|
||||
const SizedBox(width: 6),
|
||||
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[3]), // Tank
|
||||
const SizedBox(width: 6),
|
||||
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[7]), // Minaret
|
||||
const SizedBox(width: 6),
|
||||
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[8]), // Grand Minaret
|
||||
const SizedBox(width: 6),
|
||||
_buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[4]), // Flagpole
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickPresetChip(OpticalRangefinderController ctrl, TargetPreset preset) {
|
||||
final isSelected = !ctrl.isCustomTarget.value && ctrl.selectedTarget.value.id == preset.id;
|
||||
return InkWell(
|
||||
onTap: () => ctrl.selectTarget(preset),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF0071E3) : const Color(0xFF020617),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: isSelected ? const Color(0xFF00F0FF) : Colors.white12),
|
||||
),
|
||||
child: Text(
|
||||
'${preset.titleAr.split(' ').first} (${preset.heightMeters.toStringAsFixed(1)}م)',
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : const Color(0xFF94A3B8),
|
||||
fontSize: 10.5,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Modals & Sheets ────────────────────────────────────────────────────────
|
||||
|
||||
void _openTargetPickerSheet(BuildContext context, OpticalRangefinderController ctrl) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: const Color(0xFF090E17),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
side: BorderSide(color: Color(0xFF0071E3), width: 1.5),
|
||||
),
|
||||
builder: (ctx) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'مكتبة الأهداف التكتيكية الميدانية',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white54, size: 18),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Flexible(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
...CameraSensorCalibrationService.standardTargets.map((preset) {
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.adjust, color: Color(0xFF00F0FF), size: 18),
|
||||
),
|
||||
title: Text(preset.titleAr, style: const TextStyle(color: Colors.white, fontSize: 12.5)),
|
||||
subtitle: Text(
|
||||
'التصنيف: ${preset.categoryAr} • الارتفاع: ${preset.heightMeters} متر',
|
||||
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_left, color: Colors.white24),
|
||||
onTap: () {
|
||||
ctrl.selectTarget(preset);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
);
|
||||
}),
|
||||
const Divider(color: Colors.white12),
|
||||
ListTile(
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.edit, color: Color(0xFFFBBF24), size: 18),
|
||||
),
|
||||
title: const Text('إدخال ارتفاع يدوي مخصص...', style: TextStyle(color: Color(0xFFFBBF24), fontSize: 12.5, fontWeight: FontWeight.bold)),
|
||||
subtitle: const Text('لتحديد ارتفاع أي معلم أو جبل أو سارية بالأمتار بدقة', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10)),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
_showCustomHeightDialog(context, ctrl);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showCustomHeightDialog(BuildContext context, OpticalRangefinderController ctrl) {
|
||||
final textCtrl = TextEditingController(text: ctrl.customHeight.value.toStringAsFixed(1));
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: const Color(0xFF090E17),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(color: Color(0xFF0071E3)),
|
||||
),
|
||||
title: const Text('تعيين ارتفاع الهدف التكتيكي', style: TextStyle(color: Colors.white, fontSize: 14)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'أدخل الارتفاع التقديري الحقيقي للمعلم بالأمتار لحساب المدى البصري بدقة:',
|
||||
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 11),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
TextField(
|
||||
controller: textCtrl,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
decoration: InputDecoration(
|
||||
suffixText: 'متر',
|
||||
suffixStyle: const TextStyle(color: Color(0xFF00F0FF)),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF020617),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('إلغاء', style: TextStyle(color: Colors.white54)),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0071E3)),
|
||||
onPressed: () {
|
||||
final val = double.tryParse(textCtrl.text);
|
||||
if (val != null && val > 0) {
|
||||
ctrl.setCustomTargetHeight(val);
|
||||
}
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text('حفظ واحتساب', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openSensorInspectorDialog(BuildContext context, OpticalRangefinderController ctrl) {
|
||||
final profile = ctrl.sensorProfile.value;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: const Color(0xFF090E17),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: const BorderSide(color: Color(0xFF38BDF8), width: 1.5),
|
||||
),
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.memory, color: Color(0xFF38BDF8), size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('فحص ومعايرة مستشعر الكاميرا', style: TextStyle(color: Colors.white, fontSize: 13.5, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInspectorRow('اسم المستشعر:', profile.deviceName),
|
||||
const Divider(color: Colors.white12, height: 12),
|
||||
_buildInspectorRow('البعد البؤري الفيزيائي:', '${profile.focalLengthMm.toStringAsFixed(2)} mm'),
|
||||
const Divider(color: Colors.white12, height: 12),
|
||||
_buildInspectorRow('المكافئ البؤري (35mm Equiv):', '${profile.focalLength35mmEquiv.toStringAsFixed(1)} mm'),
|
||||
const Divider(color: Colors.white12, height: 12),
|
||||
_buildInspectorRow('زاوية الرؤية الأفقية (HFOV):', '${profile.horizontalFovDegrees.toStringAsFixed(1)}°'),
|
||||
const Divider(color: Colors.white12, height: 12),
|
||||
_buildInspectorRow('أبعاد المستشعر (W x H):', '${profile.sensorWidthMm.toStringAsFixed(2)} x ${profile.sensorHeightMm.toStringAsFixed(2)} mm'),
|
||||
const Divider(color: Colors.white12, height: 12),
|
||||
_buildInspectorRow('حالة المعايرة البصرية:', 'معاير تلقائياً 100% (Calibrated)'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0071E3)),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('تم', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInspectorRow(String label, String value) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 11)),
|
||||
Text(value, style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Painter ──────────────────────────────────────────────────────────────────
|
||||
|
||||
class _MilReticulePainter extends CustomPainter {
|
||||
final double centerX;
|
||||
final double centerY;
|
||||
final double bracketHalfHeight;
|
||||
final double bracketWidth;
|
||||
final double rollDeg;
|
||||
|
||||
_MilReticulePainter({
|
||||
required this.centerX,
|
||||
required this.centerY,
|
||||
required this.bracketHalfHeight,
|
||||
required this.bracketWidth,
|
||||
required this.rollDeg,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final reticulePaint = Paint()
|
||||
..color = const Color(0xFF00F0FF).withAlpha(200)
|
||||
..strokeWidth = 1.5
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final bracketPaint = Paint()
|
||||
..color = const Color(0xFFFBBF24)
|
||||
..strokeWidth = 2.5
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final centerPaint = Paint()
|
||||
..color = const Color(0xFF00F0FF)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// Center dot
|
||||
canvas.drawCircle(Offset(centerX, centerY), 3.0, centerPaint);
|
||||
|
||||
// Crosshair horizontal and vertical lines with mil marks
|
||||
const tickLen = 6.0;
|
||||
for (int i = -4; i <= 4; i++) {
|
||||
if (i == 0) continue;
|
||||
final x = centerX + i * 20.0;
|
||||
canvas.drawLine(Offset(x, centerY - tickLen), Offset(x, centerY + tickLen), reticulePaint);
|
||||
final y = centerY + i * 20.0;
|
||||
canvas.drawLine(Offset(centerX - tickLen, y), Offset(centerX + tickLen, y), reticulePaint);
|
||||
}
|
||||
|
||||
// Top Bracket
|
||||
final topY = centerY - bracketHalfHeight;
|
||||
canvas.drawLine(Offset(centerX - bracketWidth / 2, topY), Offset(centerX + bracketWidth / 2, topY), bracketPaint);
|
||||
canvas.drawLine(Offset(centerX - bracketWidth / 2, topY), Offset(centerX - bracketWidth / 2, topY + 12), bracketPaint);
|
||||
canvas.drawLine(Offset(centerX + bracketWidth / 2, topY), Offset(centerX + bracketWidth / 2, topY + 12), bracketPaint);
|
||||
|
||||
// Bottom Bracket
|
||||
final botY = centerY + bracketHalfHeight;
|
||||
canvas.drawLine(Offset(centerX - bracketWidth / 2, botY), Offset(centerX + bracketWidth / 2, botY), bracketPaint);
|
||||
canvas.drawLine(Offset(centerX - bracketWidth / 2, botY), Offset(centerX - bracketWidth / 2, botY - 12), bracketPaint);
|
||||
canvas.drawLine(Offset(centerX + bracketWidth / 2, botY), Offset(centerX + bracketWidth / 2, botY - 12), bracketPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _MilReticulePainter oldDelegate) {
|
||||
return oldDelegate.bracketHalfHeight != bracketHalfHeight ||
|
||||
oldDelegate.bracketWidth != bracketWidth ||
|
||||
oldDelegate.rollDeg != rollDeg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PersistentTacticalSheetWrapper extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final bool isMinimized;
|
||||
final VoidCallback onToggleMinimize;
|
||||
final VoidCallback onClose;
|
||||
final Widget child;
|
||||
|
||||
const PersistentTacticalSheetWrapper({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.isMinimized,
|
||||
required this.onToggleMinimize,
|
||||
required this.onClose,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: isMinimized ? 60.0 : MediaQuery.of(context).size.height * 0.85,
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xF50F172A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: Border(top: BorderSide(color: Color(0xFF0071E3), width: 2)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header (always visible)
|
||||
GestureDetector(
|
||||
onVerticalDragUpdate: (details) {
|
||||
if (details.delta.dy > 5 && !isMinimized) {
|
||||
onToggleMinimize();
|
||||
} else if (details.delta.dy < -5 && isMinimized) {
|
||||
onToggleMinimize();
|
||||
}
|
||||
},
|
||||
onTap: onToggleMinimize,
|
||||
child: Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
color: Colors.transparent, // Capture taps
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: const Color(0xFF38BDF8), size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isMinimized ? Icons.expand_less : Icons.expand_more,
|
||||
color: Colors.white70,
|
||||
),
|
||||
onPressed: onToggleMinimize,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white70),
|
||||
onPressed: onClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Body (animates height)
|
||||
if (!isMinimized)
|
||||
Flexible(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(0)),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ class _TacticalArtillerySheetState extends State<TacticalArtillerySheet> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF090E17),
|
||||
color: Color(0xFF090E1A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
border: Border(top: BorderSide(color: Color(0xFF0071E3), width: 1.5)),
|
||||
boxShadow: [
|
||||
@@ -143,7 +143,7 @@ class _TacticalArtillerySheetState extends State<TacticalArtillerySheet> {
|
||||
|
||||
// ── Weapon Selector ───────────────────────────────────
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
@@ -339,7 +339,7 @@ class _TacticalArtillerySheetState extends State<TacticalArtillerySheet> {
|
||||
height: 120,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF020617),
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
@@ -394,7 +394,7 @@ class _TacticalArtillerySheetState extends State<TacticalArtillerySheet> {
|
||||
|
||||
// Crest Clearance Status
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _solution!.isCrestClear
|
||||
? const Color(0x3310B981)
|
||||
|
||||
@@ -19,72 +19,73 @@ class TacticalCrosshair extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Reticle Canvas
|
||||
CustomPaint(
|
||||
size: Size.infinite,
|
||||
painter: _CrosshairPainter(
|
||||
isLocked: isAimLocked,
|
||||
return IgnorePointer(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Reticle Canvas
|
||||
CustomPaint(
|
||||
size: Size.infinite,
|
||||
painter: _CrosshairPainter(
|
||||
isLocked: isAimLocked,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Top Azimuth & Mils Telemetry Strip
|
||||
Positioned(
|
||||
top: 16,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(204),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isAimLocked ? const Color(0xFF22C55E) : const Color(0xFF00F0FF),
|
||||
width: 1.5,
|
||||
// Top Azimuth & Mils Telemetry Strip
|
||||
Positioned(
|
||||
top: 16,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(204),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isAimLocked ? const Color(0xFF22C55E) : const Color(0xFF00F0FF),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isAimLocked
|
||||
? const Color(0xFF22C55E).withAlpha(102)
|
||||
: const Color(0xFF00F0FF).withAlpha(102),
|
||||
blurRadius: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isAimLocked
|
||||
? const Color(0xFF22C55E).withAlpha(102)
|
||||
: const Color(0xFF00F0FF).withAlpha(102),
|
||||
blurRadius: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.explore,
|
||||
size: 16,
|
||||
color: isAimLocked ? const Color(0xFF4ADE80) : const Color(0xFF00F0FF),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'السمت: ${AngleFormatter.format(trueAzimuthDeg, angleUnit, includeLabel: false)}',
|
||||
style: TextStyle(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.explore,
|
||||
size: 16,
|
||||
color: isAimLocked ? const Color(0xFF4ADE80) : const Color(0xFF00F0FF),
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(width: 1, height: 14, color: Colors.white24),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'مغناطيسي: ${headingDeg.toStringAsFixed(0).padLeft(3, '0')}°',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF94A3B8),
|
||||
fontSize: 10.5,
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'الاتجاه: ${AngleFormatter.format(trueAzimuthDeg, angleUnit, includeLabel: false)}',
|
||||
style: TextStyle(
|
||||
color: isAimLocked ? const Color(0xFF4ADE80) : const Color(0xFF00F0FF),
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
Container(width: 1, height: 14, color: Colors.white24),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'مغناطيسي: ${headingDeg.toStringAsFixed(0).padLeft(3, '0')}°',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF94A3B8),
|
||||
fontSize: 10.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Target Aiming Name Label under reticle
|
||||
if (targetName != null)
|
||||
@@ -128,6 +129,7 @@ class TacticalCrosshair extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ class TacticalDrawer extends StatefulWidget {
|
||||
final AngleUnit currentAngleUnit;
|
||||
final Function(AngleUnit) onAngleUnitChanged;
|
||||
final VoidCallback onOpenResectionHud;
|
||||
final VoidCallback? onStartRangefinderMode;
|
||||
final VoidCallback onStartRoutingMode;
|
||||
final VoidCallback onStartLosMode;
|
||||
final VoidCallback onStartViewshedMode;
|
||||
@@ -17,6 +18,7 @@ class TacticalDrawer extends StatefulWidget {
|
||||
final VoidCallback onStartSymbolsMode;
|
||||
final VoidCallback onStartOverlaysMode;
|
||||
final VoidCallback onLandmarksSynced;
|
||||
final VoidCallback? onClearMap;
|
||||
final bool showContours;
|
||||
final Function(bool) onToggleContours;
|
||||
|
||||
@@ -25,6 +27,7 @@ class TacticalDrawer extends StatefulWidget {
|
||||
required this.currentAngleUnit,
|
||||
required this.onAngleUnitChanged,
|
||||
required this.onOpenResectionHud,
|
||||
this.onStartRangefinderMode,
|
||||
required this.onStartRoutingMode,
|
||||
required this.onStartLosMode,
|
||||
required this.onStartViewshedMode,
|
||||
@@ -35,6 +38,7 @@ class TacticalDrawer extends StatefulWidget {
|
||||
required this.onStartSymbolsMode,
|
||||
required this.onStartOverlaysMode,
|
||||
required this.onLandmarksSynced,
|
||||
this.onClearMap,
|
||||
this.showContours = true,
|
||||
required this.onToggleContours,
|
||||
});
|
||||
@@ -130,7 +134,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
|
||||
Icon(Icons.explore, size: 16, color: Color(0xFF38BDF8)),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'نظام قياس الزوايا والسمت:',
|
||||
'نظام قياس الزوايا والاتجاه:',
|
||||
style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
@@ -178,10 +182,64 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
|
||||
),
|
||||
),
|
||||
|
||||
// 2. Visual Resection
|
||||
// 0. Clear Map & Reset Layers (مسح وتنظيف الخريطة)
|
||||
if (widget.onClearMap != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
widget.onClearMap!();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x22F43F5E),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFF43F5E).withAlpha(120)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.cleaning_services_rounded, color: Color(0xFFF43F5E), size: 20),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'مسح وتنظيف الخريطة (Clear Map)',
|
||||
style: TextStyle(color: Color(0xFFF43F5E), fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'تصفير كافة الأدوات والرسوم والأهداف السابقة',
|
||||
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 1. Optical Rangefinder (قياس المسافة البصري بدون GPS)
|
||||
_buildOperationTile(
|
||||
icon: Icons.center_focus_strong,
|
||||
iconColor: const Color(0xFF00F0FF),
|
||||
title: 'قياس المسافة البصري (بدون GPS)',
|
||||
subtitle: 'حساب المسافة والارتفاع بالتقريب والتبعيد البصري',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
widget.onStartRangefinderMode?.call();
|
||||
},
|
||||
),
|
||||
|
||||
// 2. Visual Resection (التقاطع البصري العكسي)
|
||||
_buildOperationTile(
|
||||
icon: Icons.camera_alt,
|
||||
iconColor: const Color(0xFF00F0FF),
|
||||
iconColor: const Color(0xFF38BDF8),
|
||||
title: 'التقاطع البصري (GPS-Denied HUD)',
|
||||
subtitle: 'تسديد الكاميرا وتحديد الإحداثيات بالرماية',
|
||||
onTap: () {
|
||||
|
||||
@@ -74,7 +74,7 @@ class _TacticalHlzSheetState extends State<TacticalHlzSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF090E17),
|
||||
color: Color(0xFF090E1A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
border: Border(top: BorderSide(color: Color(0xFF10B981), width: 1.5)),
|
||||
boxShadow: [
|
||||
@@ -240,9 +240,9 @@ class _TacticalHlzSheetState extends State<TacticalHlzSheet> {
|
||||
else if (_result != null) ...[
|
||||
// Suitability Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _result!.gradeColor.withAlpha(40),
|
||||
color: _result!.gradeColor.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: _result!.gradeColor),
|
||||
),
|
||||
@@ -315,7 +315,7 @@ class _TacticalHlzSheetState extends State<TacticalHlzSheet> {
|
||||
},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF10B981).withAlpha(40) : const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
|
||||
@@ -71,7 +71,7 @@ class _TacticalIsochroneSheetState extends State<TacticalIsochroneSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF090E17),
|
||||
color: Color(0xFF090E1A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
border: Border(top: BorderSide(color: Color(0xFFA855F7), width: 1.5)),
|
||||
boxShadow: [
|
||||
@@ -215,7 +215,7 @@ class _TacticalIsochroneSheetState extends State<TacticalIsochroneSheet> {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 3),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: r.ringColor.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
||||
@@ -207,9 +207,9 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.88,
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 12, left: 16, right: 16, bottom: 20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xF80F172A),
|
||||
color: Color(0xFF090E1A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: Border(top: BorderSide(color: Color(0xFF0071E3), width: 2.5)),
|
||||
boxShadow: [BoxShadow(color: Colors.black87, blurRadius: 30)],
|
||||
@@ -342,7 +342,7 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x661E293B),
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0x3338BDF8)),
|
||||
),
|
||||
@@ -403,7 +403,7 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x661E293B),
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0x33F59E0B)),
|
||||
),
|
||||
@@ -469,7 +469,7 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x661E293B),
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0x3338BDF8)),
|
||||
),
|
||||
@@ -521,7 +521,7 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x661E293B),
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0x33F59E0B)),
|
||||
),
|
||||
@@ -577,9 +577,9 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
|
||||
// Optic Heights Adjustment (ارتفاع البصريات وسواري الرصد)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x991E293B),
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white10),
|
||||
),
|
||||
@@ -623,7 +623,7 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
|
||||
// Tactical Metrics Data Summary
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -657,9 +657,9 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
const SizedBox(height: 8),
|
||||
// Ground & air / LOS geometry detail row
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0B1220),
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isVisible ? const Color(0x3322C55E) : const Color(0x44EF4444),
|
||||
@@ -753,7 +753,7 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0071E3),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
@@ -769,7 +769,7 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF38BDF8),
|
||||
side: const BorderSide(color: Color(0xFF0071E3)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
@@ -794,7 +794,7 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
const PopupMenuItem(value: 25.0, child: Text('منطاد استطلاع / درون (25 متر)', style: TextStyle(color: Colors.white, fontSize: 11))),
|
||||
],
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0071E3).withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
@@ -815,7 +815,7 @@ class _TacticalLosSheetState extends State<TacticalLosSheet> with SingleTickerPr
|
||||
Widget _buildSampleInspectorCard(OfflineLosSample s) {
|
||||
final mil = MilitaryGridUtils.fromLatLng(LatLng(s.lat, s.lng));
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -879,7 +879,7 @@ class _LosProfileChart extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(6, 8, 6, 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0B1220),
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0x330071E3)),
|
||||
),
|
||||
@@ -887,7 +887,7 @@ class _LosProfileChart extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -70,7 +70,7 @@ class _TacticalMinefieldSheetState extends State<TacticalMinefieldSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF090E17),
|
||||
color: Color(0xFF090E1A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
border: Border(top: BorderSide(color: Color(0xFFF59E0B), width: 1.5)),
|
||||
boxShadow: [
|
||||
@@ -301,9 +301,9 @@ class _TacticalMinefieldSheetState extends State<TacticalMinefieldSheet> {
|
||||
|
||||
// Breaching Corridor Info
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x3310B981),
|
||||
color: const Color(0xFF10B981).withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF10B981)),
|
||||
),
|
||||
@@ -346,7 +346,7 @@ class _TacticalMinefieldSheetState extends State<TacticalMinefieldSheet> {
|
||||
},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFFF59E0B).withAlpha(40) : const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
|
||||
@@ -18,7 +18,7 @@ class TacticalOverlaysSheet extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF090E17),
|
||||
color: Color(0xFF090E1A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
border: Border(top: BorderSide(color: Color(0xFF38BDF8), width: 1.5)),
|
||||
boxShadow: [
|
||||
|
||||
@@ -4,6 +4,7 @@ import '../models/landmark.dart';
|
||||
import '../services/landmark_database.dart';
|
||||
import '../services/local_sqlite_db.dart';
|
||||
import '../services/offline_routing_engine.dart';
|
||||
import '../services/offline_routing_package_service.dart';
|
||||
import '../services/tactical_api_service.dart';
|
||||
|
||||
class TacticalRoutePlannerSheet extends StatefulWidget {
|
||||
@@ -125,16 +126,16 @@ class _TacticalRoutePlannerSheetState extends State<TacticalRoutePlannerSheet> {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
widget.onClose();
|
||||
widget.onRouteConfirmed(_origin, _destination!, _selectedProfile, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 14, left: 16, right: 16, bottom: 24),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xF50F172A),
|
||||
color: Color(0xFF090E1A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: Border(top: BorderSide(color: Color(0xFF0071E3), width: 2)),
|
||||
boxShadow: [BoxShadow(color: Colors.black87, blurRadius: 30)],
|
||||
@@ -152,7 +153,7 @@ class _TacticalRoutePlannerSheetState extends State<TacticalRoutePlannerSheet> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x330071E3),
|
||||
color: const Color(0xFF0071E3).withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF0071E3)),
|
||||
),
|
||||
@@ -183,6 +184,50 @@ class _TacticalRoutePlannerSheetState extends State<TacticalRoutePlannerSheet> {
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Real road network engine status (on-device Valhalla package)
|
||||
ValueListenableBuilder<RoutingPackageStatus>(
|
||||
valueListenable: OfflineRoutingPackageService.status,
|
||||
builder: (context, pkgStatus, _) {
|
||||
return FutureBuilder<bool>(
|
||||
future: OfflineRoutingPackageService.isInstalled(),
|
||||
builder: (context, snapshot) {
|
||||
final installed = snapshot.data ?? false;
|
||||
final color = installed ? const Color(0xFF22C55E) : const Color(0xFFF59E0B);
|
||||
final label = installed
|
||||
? 'التوجيه المحلي: شبكة الطرق الحقيقية للأردن + SRTM جاهزة (بدون إنترنت)'
|
||||
: 'التوجيه المحلي الدقيق غير مثبت — نزّل حزمة الطرق من إعدادات الحزم';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(installed ? Icons.route : Icons.download_rounded, color: color, size: 14),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(color: color, fontSize: 9.5),
|
||||
),
|
||||
),
|
||||
if (!installed && pkgStatus == RoutingPackageStatus.downloading)
|
||||
SizedBox(
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: CircularProgressIndicator(strokeWidth: 1.5, color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Origin & Destination Box
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -214,10 +259,7 @@ class _TacticalRoutePlannerSheetState extends State<TacticalRoutePlannerSheet> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.touch_app, size: 18, color: Color(0xFF38BDF8)),
|
||||
tooltip: 'تحديد نقطة البداية على الخريطة',
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
widget.onPickOriginOnMap();
|
||||
},
|
||||
onPressed: widget.onPickOriginOnMap,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -247,7 +289,7 @@ class _TacticalRoutePlannerSheetState extends State<TacticalRoutePlannerSheet> {
|
||||
_destSearchCtrl.clear();
|
||||
_onSearch('');
|
||||
},
|
||||
)
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
@@ -255,10 +297,7 @@ class _TacticalRoutePlannerSheetState extends State<TacticalRoutePlannerSheet> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.touch_app, size: 18, color: Color(0xFFF59E0B)),
|
||||
tooltip: 'تحديد الهدف على الخريطة',
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
widget.onPickDestinationOnMap();
|
||||
},
|
||||
onPressed: widget.onPickDestinationOnMap,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -326,7 +365,7 @@ class _TacticalRoutePlannerSheetState extends State<TacticalRoutePlannerSheet> {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0071E3),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||
padding: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
@@ -342,7 +381,7 @@ class _TacticalRoutePlannerSheetState extends State<TacticalRoutePlannerSheet> {
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _selectedProfile = prof),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF0071E3) : const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
|
||||
@@ -15,18 +15,17 @@ class TacticalRoutePreviewCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
left: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF50F172A),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: const Color(0xFF38BDF8), width: 1.5),
|
||||
boxShadow: const [BoxShadow(color: Colors.black87, blurRadius: 25)],
|
||||
),
|
||||
// ملاحظة: هذا الودجت يوضع دائماً داخل Positioned على مستوى الشاشة
|
||||
// (خارج Obx) — لا يُعِد Positioned من الداخل لتجنب خطأ
|
||||
// "Positioned inside non-Stack / hit test render box with no size".
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF50F172A),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: const Color(0xFF38BDF8), width: 1.5),
|
||||
boxShadow: const [BoxShadow(color: Colors.black87, blurRadius: 25)],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -119,7 +118,6 @@ class TacticalRoutePreviewCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class TacticalSymbolsSheet extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF090E17),
|
||||
color: Color(0xFF090E1A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
border: Border(top: BorderSide(color: Color(0xFF0071E3), width: 1.5)),
|
||||
boxShadow: [
|
||||
@@ -164,9 +164,9 @@ class TacticalSymbolsSheet extends StatelessWidget {
|
||||
final s = placedSymbols[i];
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F172A),
|
||||
color: s.color.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: s.color.withAlpha(100)),
|
||||
),
|
||||
|
||||
@@ -134,9 +134,9 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
|
||||
final obsElev = JordanDemSurface.elevationAt(_currentObs.latitude, _currentObs.longitude);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF0F172A),
|
||||
color: Color(0xFF090E1A),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: Border(top: BorderSide(color: Color(0xFF38BDF8), width: 1.5)),
|
||||
),
|
||||
@@ -166,7 +166,7 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x3322C55E),
|
||||
color: const Color(0xFF22C55E).withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFF22C55E)),
|
||||
),
|
||||
@@ -208,9 +208,9 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
|
||||
// Metrics Card
|
||||
if (_report != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white10),
|
||||
),
|
||||
@@ -308,7 +308,7 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF38BDF8),
|
||||
side: const BorderSide(color: Color(0xFF38BDF8)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
icon: Icon(_showCoordInputs ? Icons.expand_less : Icons.edit_location_alt, size: 16),
|
||||
@@ -322,7 +322,7 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0284C7),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
icon: const Icon(Icons.touch_app, size: 16),
|
||||
@@ -460,7 +460,7 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF22C55E),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('تطبيق الإحداثيات وإعادة الرصد', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
@@ -485,7 +485,7 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
|
||||
_computeViewshed();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF0284C7) : const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
||||
Reference in New Issue
Block a user