chore: add build artifacts, map assets, and routing data
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user