diff --git a/lib/pages/map_survey/presentations/views/map_survey_view.dart b/lib/pages/map_survey/presentations/views/map_survey_view.dart index 1433e20..496cb5b 100644 --- a/lib/pages/map_survey/presentations/views/map_survey_view.dart +++ b/lib/pages/map_survey/presentations/views/map_survey_view.dart @@ -12,6 +12,7 @@ import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_surv import 'package:terepi_seged/pages/map_survey/presentations/views/settings_dialog.dart'; import 'package:terepi_seged/utils/rive_utils.dart'; import 'package:terepi_seged/widgets/coordinate_panel.dart'; +import 'package:terepi_seged/widgets/map_bottom_panel.dart'; import 'package:terepi_seged/widgets/save_point_fab.dart'; import 'package:terepi_seged/widgets/shared_map_widgets.dart'; @@ -26,26 +27,31 @@ class MapSurveyView extends GetView { const SharedMapWidget(), Positioned( top: 8, - right: 8, + right: 60, left: 8, child: CoordinatePanel.fromController(controller), ), - Positioned(top: 8, left: 0, right: 0, child: _ModeSelector()), + // Positioned(top: 8, left: 0, right: 0, child: _ModeSelector()), + // Positioned( + // bottom: 80, + // left: 8, + // right: 8, + // child: Obx( + // () => controller.mode.value == MapSurveyMode.stakeout + // ? _StakeoutPanel() // ΔY, ΔX, távolság, irányszög + // : const SizedBox.shrink(), + // ), + // ), + // Positioned( + // bottom: 16, + // right: 16, + // child: SavePointFab(controller: controller), + // ), Positioned( - bottom: 80, - left: 8, - right: 8, - child: Obx( - () => controller.mode.value == MapSurveyMode.stakeout - ? _StakeoutPanel() // ΔY, ΔX, távolság, irányszög - : const SizedBox.shrink(), - ), - ), - Positioned( - bottom: 16, - right: 16, - child: SavePointFab(controller: controller), - ), + bottom: 0, + left: 0, + right: 0, + child: MapBottomPanel(controller: controller)) ]); } } diff --git a/lib/pages/shell/presentations/controllers/shell_controller.dart b/lib/pages/shell/presentations/controllers/shell_controller.dart index 7d3bc5a..15e340a 100644 --- a/lib/pages/shell/presentations/controllers/shell_controller.dart +++ b/lib/pages/shell/presentations/controllers/shell_controller.dart @@ -4,6 +4,7 @@ class ShellController extends GetxController { static ShellController get to => Get.find(); final currentIndex = 0.obs; + final isNavBarVisible = true.obs; static const titles = [ 'Térkép', @@ -24,4 +25,8 @@ class ShellController extends GetxController { void goToSurvey() => goToTab(1); void goToTracking() => goToTab(2); void goToData() => goToTab(3); + + void showNavBar() => isNavBarVisible.value = true; + void hideNavBar() => isNavBarVisible.value = false; + void toggleNavBar() => isNavBarVisible.value = !isNavBarVisible.value; } diff --git a/lib/pages/shell/presentations/views/shell_view.dart b/lib/pages/shell/presentations/views/shell_view.dart index 07e1f93..2885585 100644 --- a/lib/pages/shell/presentations/views/shell_view.dart +++ b/lib/pages/shell/presentations/views/shell_view.dart @@ -46,30 +46,37 @@ class ShellView extends GetView { index: controller.currentIndex.value, children: _pages, )), - bottomNavigationBar: Obx(() => NavigationBar( - selectedIndex: controller.currentIndex.value, - onDestinationSelected: controller.goToTab, - destinations: const [ - NavigationDestination( - icon: Icon(Icons.map_outlined), - selectedIcon: Icon(Icons.map), - label: 'Térkép', - ), - NavigationDestination( - icon: Icon(Icons.gps_fixed), - label: 'Mérés', - ), - NavigationDestination( - icon: Icon(Icons.route), - label: 'Track', - ), - NavigationDestination( - icon: Icon(Icons.table_chart_outlined), - selectedIcon: Icon(Icons.table_chart), - label: 'Adatok', - ), - ], - )), + bottomNavigationBar: Obx( + () => AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: controller.isNavBarVisible.value + ? NavigationBar( + selectedIndex: controller.currentIndex.value, + onDestinationSelected: controller.goToTab, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.map_outlined), + selectedIcon: Icon(Icons.map), + label: 'Térkép', + ), + NavigationDestination( + icon: Icon(Icons.gps_fixed), + label: 'Mérés', + ), + NavigationDestination( + icon: Icon(Icons.route), + label: 'Track', + ), + NavigationDestination( + icon: Icon(Icons.table_chart_outlined), + selectedIcon: Icon(Icons.table_chart), + label: 'Adatok', + ), + ], + ) + : const SizedBox.shrink()), + ), ); } } diff --git a/lib/widgets/gnss_status_chip.dart b/lib/widgets/gnss_status_chip.dart index b6bf637..33594e1 100644 --- a/lib/widgets/gnss_status_chip.dart +++ b/lib/widgets/gnss_status_chip.dart @@ -27,66 +27,23 @@ class GnssStatusChip extends StatelessWidget { return Obx(() { final connState = GnssService.to.connectionState.value; final quality = GnssService.to.gpsQuality.value; - final device = GnssDeviceService.to.selectedDevice.value; - final sats = GnssService.to.satelliteCount.value; - - final isConnected = connState == GnssConnectionState.connected; + final isConn = connState == GnssConnectionState.connected; final isConnecting = connState == GnssConnectionState.connecting; - final color = _chipColor(isConnected, quality); - final label = _chipLabel(connState, quality, device?.name); + final color = _chipColor(isConn, quality); - return GestureDetector( - onTap: () => GnssDevicePickerDialog.show(), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: color.withOpacity(0.15), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: color.withOpacity(0.5)), - ), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - // Állapot ikon - if (isConnecting) - SizedBox( - width: 10, - height: 10, - child: CircularProgressIndicator( - strokeWidth: 1.5, - color: color, - ), - ) - else - Icon( - _chipIcon(isConnected, quality), - size: 12, - color: color, - ), - - const SizedBox(width: 4), - - // Felirat - Text( - label, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: color, - ), - ), - - // Műholdak száma (csak ha van fix) - if (isConnected && quality > 0) ...[ - const SizedBox(width: 4), - Text( - '($sats)', - style: TextStyle( - fontSize: 10, - color: color.withOpacity(0.8), - ), - ), - ], - ]), + return Tooltip( + // Hosszú nyomásra szöveg jelenik meg + message: _chipLabel(connState, quality, ''), + child: IconButton( + icon: isConnecting + ? SizedBox( + width: 18, + height: 18, + child: + CircularProgressIndicator(strokeWidth: 2, color: color), + ) + : Icon(_chipIcon(isConn, quality), color: color), + onPressed: () => GnssDevicePickerDialog.show(), ), ); }); diff --git a/lib/widgets/map_bottom_panel.dart b/lib/widgets/map_bottom_panel.dart new file mode 100644 index 0000000..e48b563 --- /dev/null +++ b/lib/widgets/map_bottom_panel.dart @@ -0,0 +1,426 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; +import 'package:terepi_seged/pages/shell/presentations/controllers/shell_controller.dart'; + +import '../services/gnss/gnss_service.dart'; + +/// Összecsukható alsó panel — bemérés és kitűzés módváltóval. +/// +/// Összecsukva: mód chip-ek + kis mentés gomb (nem takarja a térképet). +/// Kitűzés módban automatikusan kinyílik a ΔY/ΔX/távolság panellel. +/// +/// Használat a Stack aljában: +/// ```dart +/// Positioned( +/// bottom: 0, left: 0, right: 0, +/// child: SurveyBottomPanel(controller: controller), +/// ) +/// ``` +class MapBottomPanel extends StatefulWidget { + final dynamic controller; + + const MapBottomPanel({super.key, required this.controller}); + + @override + State createState() => _SurveyBottomPanelState(); +} + +class _SurveyBottomPanelState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animCtrl; + late Animation _heightAnim; + + // Panel magasságok + static const _collapsedHeight = 68.0; + static const _expandedHeight = 220.0; + + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + _animCtrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 250), + ); + _heightAnim = Tween( + begin: _collapsedHeight, + end: _expandedHeight, + ).animate(CurvedAnimation( + parent: _animCtrl, + curve: Curves.easeOutCubic, + )); + + // Ha kitűzés módba lépünk → automatikus kinyitás + ever(widget.controller.mode as Rx, (mode) { + if (mode == MapSurveyMode.stakeout.index && !_isExpanded) { + _expand(); + } + }); + } + + @override + void dispose() { + _animCtrl.dispose(); + super.dispose(); + } + + void _toggle() { + if (_isExpanded) { + _collapse(); + } else { + _expand(); + } + } + + void _expand() { + _animCtrl.forward(); + setState(() => _isExpanded = true); + ShellController.to.hideNavBar(); + } + + void _collapse() { + _animCtrl.reverse(); + setState(() => _isExpanded = false); + ShellController.to.showNavBar(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _heightAnim, + builder: (context, child) => Container( + height: _heightAnim.value, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.88), + borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, -2), + ), + ], + ), + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Handle — húzásra nyíl/csuk ───────────────────── + GestureDetector( + onTap: _toggle, + onVerticalDragEnd: (d) { + // Felfelé húzás → kinyit, lefelé → csuk + if (d.primaryVelocity! < -100) _expand(); + if (d.primaryVelocity! > 100) _collapse(); + }, + behavior: HitTestBehavior.opaque, + child: SizedBox( + width: double.infinity, + height: 20, + child: Center( + child: Container( + width: 36, + height: 3, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + ), + + // ── Módváltó + mentés gomb (mindig látható) ───────── + Padding( + padding: EdgeInsets.fromLTRB( + 10, 6, 10, 6 + MediaQuery.of(context).padding.bottom), + child: _ModeRow( + controller: widget.controller, + onSave: () => _showSaveDialog(context), + ), + ), + + // ── Kitűzési adatok (csak kinyitva) ───────────────── + if (_isExpanded && _animCtrl.value > 0.5) + Expanded( + child: _StakeoutContent( + controller: widget.controller, + onSave: () => _showSaveDialog(context), + ), + ), + ], + ), + ), + ), + ); + } + + void _showSaveDialog(BuildContext context) { + widget.controller.showAddPointDialog(); + } +} + +// ── Módváltó sor ────────────────────────────────────────────────────── + +class _ModeRow extends StatelessWidget { + final dynamic controller; + final VoidCallback onSave; + + const _ModeRow({required this.controller, required this.onSave}); + + @override + Widget build(BuildContext context) { + return Obx(() { + final mode = (controller.mode?.value ?? MapSurveyMode.measure); + + return Row(children: [ + // Bemérés chip + Expanded( + child: _ModeChip( + label: 'Bemérés', + icon: Icons.gps_fixed, + selected: mode == MapSurveyMode.measure, + onTap: () => controller.switchMode?.call(MapSurveyMode.measure), + ), + ), + const SizedBox(width: 8), + + // Kitűzés chip + Expanded( + child: _ModeChip( + label: 'Kitűzés', + icon: Icons.flag_outlined, + selected: mode == MapSurveyMode.stakeout, + onTap: () => controller.switchMode?.call(MapSurveyMode.stakeout), + ), + ), + const SizedBox(width: 8), + + // Mentés gomb — bemérés módban kis gomb + if (mode == MapSurveyMode.measure) + GestureDetector( + onTap: onSave, + child: Obx(() { + final hasfix = GnssService.to.gpsQuality.value > 0; + return Container( + width: 44, + height: 32, + decoration: BoxDecoration( + color: hasfix ? Colors.blue.shade700 : Colors.grey.shade800, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.location_on, + color: hasfix ? Colors.white : Colors.grey, + size: 20, + ), + ); + }), + ), + ]); + }); + } +} + +// ── Módváltó chip ───────────────────────────────────────────────────── + +class _ModeChip extends StatelessWidget { + final String label; + final IconData icon; + final bool selected; + final VoidCallback onTap; + + const _ModeChip({ + required this.label, + required this.icon, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 32, + decoration: BoxDecoration( + color: selected + ? Colors.blue.withOpacity(0.25) + : Colors.white.withOpacity(0.06), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: selected + ? Colors.blue.shade300.withOpacity(0.6) + : Colors.white.withOpacity(0.12), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 14, + color: selected ? Colors.blue.shade300 : Colors.white38, + ), + const SizedBox(width: 5), + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: selected ? Colors.blue.shade300 : Colors.white38, + ), + ), + ], + ), + ), + ); + } +} + +// ── Kitűzési tartalom ───────────────────────────────────────────────── + +class _StakeoutContent extends StatelessWidget { + final dynamic controller; + final VoidCallback onSave; + + const _StakeoutContent({ + required this.controller, + required this.onSave, + }); + + @override + Widget build(BuildContext context) { + return Obx(() { + final dy = (controller.deltaY as double?) ?? 0.0; + final dx = (controller.deltaX as double?) ?? 0.0; + final dist = (controller.distanceToTarget as double?) ?? 0.0; + final onTarget = (controller.isOnTarget as bool?) ?? false; + final name = (controller.targetName as RxString?) ?? ''; + + return Padding( + padding: const EdgeInsets.fromLTRB(10, 6, 10, 6), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ΔY / ΔX / Távolság + Row(children: [ + _DeltaCell('ΔY', dy), + const SizedBox(width: 6), + _DeltaCell('ΔX', dx), + const SizedBox(width: 6), + _DeltaCell('Táv', dist, alwaysPositive: true), + ]), + + const SizedBox(height: 6), + + // Célpont státusz + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: onTarget + ? Colors.green.withOpacity(0.12) + : Colors.orange.withOpacity(0.10), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: onTarget + ? Colors.green.withOpacity(0.4) + : Colors.orange.withOpacity(0.3), + ), + ), + child: Text( + onTarget + ? '✓ Célponton — rögzíthető' + : '$name · ${dist.toStringAsFixed(3)} m a céltól', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: onTarget ? Colors.greenAccent : Colors.orange, + ), + textAlign: TextAlign.center, + ), + ), + + const SizedBox(height: 6), + + // Nagy mentés gomb kitűzés módban + SizedBox( + width: double.infinity, + height: 36, + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: + onTarget ? Colors.green.shade700 : Colors.blue.shade700, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), + ), + icon: const Icon(Icons.location_on, size: 18), + label: const Text('Pont rögzítése', + style: TextStyle(fontSize: 13)), + onPressed: onSave, + ), + ), + ], + ), + ); + }); + } +} + +// ── ΔY / ΔX / Távolság cella ───────────────────────────────────────── + +class _DeltaCell extends StatelessWidget { + final String label; + final double value; + final bool alwaysPositive; + + const _DeltaCell(this.label, this.value, {this.alwaysPositive = false}); + + @override + Widget build(BuildContext context) { + final display = alwaysPositive ? value.abs() : value; + final prefix = (!alwaysPositive && value > 0) ? '+' : ''; + final color = value.abs() < 0.05 + ? Colors.greenAccent + : value.abs() < 0.5 + ? Colors.orange + : Colors.red; + + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: TextStyle( + fontSize: 9, + color: Colors.white.withOpacity(0.4), + ), + ), + Text( + '$prefix${display.toStringAsFixed(3)} m', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ); + } +}