diff --git a/lib/widgets/appbar/gnss_receiver_icon.dart b/lib/widgets/appbar/gnss_receiver_icon.dart new file mode 100644 index 0000000..f22facd --- /dev/null +++ b/lib/widgets/appbar/gnss_receiver_icon.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; + +class GnssReceiverIcon extends StatelessWidget { + final double size; + final Color? color; + final Color? surfaceColor; + + const GnssReceiverIcon({ + super.key, + this.size = 28, + this.color, + this.surfaceColor, + }); + + @override + Widget build(BuildContext context) { + final iconColor = color ?? + IconTheme.of(context).color ?? + Theme.of(context).colorScheme.onSurfaceVariant; + + final bgColor = surfaceColor ?? Theme.of(context).colorScheme.surface; + + return SizedBox( + width: size, + height: size, + child: CustomPaint( + painter: _GnssReceiverCustomIconPainter( + color: iconColor, + surfaceColor: bgColor, + ), + ), + ); + } +} + +class _GnssReceiverCustomIconPainter extends CustomPainter { + final Color color; + final Color surfaceColor; + + const _GnssReceiverCustomIconPainter({ + required this.color, + required this.surfaceColor, + }); + + @override + void paint(Canvas canvas, Size size) { + canvas.save(); + + // 100x100-as koordinátarendszerre rajzolunk, + // így az ikon jól skálázható. + canvas.scale(size.width / 100.0, size.height / 100.0); + + final mainPaint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final bandPaint = Paint() + ..color = Color.lerp(color, Colors.black, 0.35)! + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final detailPaint = Paint() + ..color = Color.lerp(color, surfaceColor, 0.72)! + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final shadowPaint = Paint() + ..color = Color.lerp(color, Colors.black, 0.18)! + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + // ── fő GNSS vevőtest ──────────────────────────────── + + final body = Path() + ..moveTo(11, 34) + ..cubicTo(11, 17, 28, 7, 50, 7) + ..cubicTo(72, 7, 89, 17, 89, 34) + ..cubicTo(89, 41, 85, 46, 79, 50) + ..cubicTo(76, 52, 75, 56, 74, 62) + ..lineTo(71, 75) + ..cubicTo(70, 84, 63, 89, 55, 89) + ..lineTo(45, 89) + ..cubicTo(37, 89, 30, 84, 29, 75) + ..lineTo(26, 62) + ..cubicTo(25, 56, 24, 52, 21, 50) + ..cubicTo(15, 46, 11, 41, 11, 34) + ..close(); + + canvas.drawPath(body, mainPaint); + + // ── felső sötét sáv ───────────────────────────────── + + final band = RRect.fromRectAndRadius( + const Rect.fromLTRB(11, 29, 89, 43), + const Radius.circular(8), + ); + + canvas.drawRRect(band, bandPaint); + + // ── kis felső kijelző / csík ───────────────────────── + + canvas.drawRRect( + RRect.fromRectAndRadius( + const Rect.fromLTRB(43, 34, 57, 36.5), + const Radius.circular(1.2), + ), + detailPaint, + ); + + // ── középső kijelző ───────────────────────────────── + + canvas.drawRRect( + RRect.fromRectAndRadius( + const Rect.fromLTRB(38, 53, 62, 68), + const Radius.circular(1.5), + ), + detailPaint, + ); + + // ── bal oldali kis jelzők ──────────────────────────── + + _drawLed(canvas, const Rect.fromLTRB(31, 55, 34.5, 58.5), detailPaint); + _drawLed(canvas, const Rect.fromLTRB(31, 61, 34.5, 64.5), detailPaint); + _drawLed(canvas, const Rect.fromLTRB(31, 67, 34.5, 70.5), detailPaint); + + // ── jobb oldali kis jelzők ─────────────────────────── + + _drawLed(canvas, const Rect.fromLTRB(65.5, 55, 69, 58.5), detailPaint); + _drawLed(canvas, const Rect.fromLTRB(65.5, 61, 69, 64.5), detailPaint); + _drawLed(canvas, const Rect.fromLTRB(65.5, 67, 69, 70.5), detailPaint); + + // ── alsó csatlakozó / bot ──────────────────────────── + + canvas.drawRect( + const Rect.fromLTRB(44, 89, 56, 98), + shadowPaint, + ); + + canvas.drawRect( + const Rect.fromLTRB(44, 98, 56, 118), + mainPaint, + ); + + // csíkok a boton + canvas.drawRect( + const Rect.fromLTRB(44, 102, 56, 104.5), + detailPaint..color = detailPaint.color.withOpacity(0.38), + ); + + canvas.drawRect( + const Rect.fromLTRB(44, 109, 56, 111), + detailPaint, + ); + + canvas.restore(); + } + + void _drawLed(Canvas canvas, Rect rect, Paint paint) { + canvas.drawRRect( + RRect.fromRectAndRadius( + rect, + const Radius.circular(0.8), + ), + paint, + ); + } + + @override + bool shouldRepaint(covariant _GnssReceiverCustomIconPainter oldDelegate) { + return oldDelegate.color != color || + oldDelegate.surfaceColor != surfaceColor; + } +} diff --git a/lib/widgets/appbar/gnss_receiver_outline_icon.dart b/lib/widgets/appbar/gnss_receiver_outline_icon.dart new file mode 100644 index 0000000..79dae2e --- /dev/null +++ b/lib/widgets/appbar/gnss_receiver_outline_icon.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; + +class GnssReceiverOutlineIcon extends StatelessWidget { + final double size; + final Color? color; + final double strokeWidth; + + const GnssReceiverOutlineIcon({ + super.key, + this.size = 26, + this.color, + this.strokeWidth = 2.1, + }); + + @override + Widget build(BuildContext context) { + final iconColor = color ?? + IconTheme.of(context).color ?? + Theme.of(context).colorScheme.onSurfaceVariant; + + return SizedBox.square( + dimension: size, + child: CustomPaint( + painter: _GnssReceiverOutlineIconPainter( + color: iconColor, + strokeWidth: strokeWidth, + ), + ), + ); + } +} + +class _GnssReceiverOutlineIconPainter extends CustomPainter { + final Color color; + final double strokeWidth; + + const _GnssReceiverOutlineIconPainter({ + required this.color, + required this.strokeWidth, + }); + + @override + void paint(Canvas canvas, Size size) { + canvas.save(); + + // 100 x 100 koordinátarendszerre rajzolunk, + // így jól skálázódik 24–32 px méret között is. + canvas.scale(size.width / 100.0, size.height / 100.0); + + final stroke = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth * 100.0 / size.width + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..isAntiAlias = true; + + final thinStroke = Paint() + ..color = color.withOpacity(0.82) + ..style = PaintingStyle.stroke + ..strokeWidth = stroke.strokeWidth * 0.72 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..isAntiAlias = true; + + // ── felső GNSS fej / antenna ──────────────────────── + // + // Kicsit hasonlít a feltöltött vevő formájára, + // de nem kitöltött, hanem outline. + final head = Path() + ..moveTo(17, 34) + ..cubicTo(17, 18, 31, 10, 50, 10) + ..cubicTo(69, 10, 83, 18, 83, 34) + ..cubicTo(83, 43, 76, 48, 68, 49) + ..lineTo(32, 49) + ..cubicTo(24, 48, 17, 43, 17, 34) + ..close(); + + canvas.drawPath(head, stroke); + + // ── alsó vevőtest ─────────────────────────────────── + final body = RRect.fromRectAndRadius( + const Rect.fromLTRB(26, 47, 74, 76), + const Radius.circular(8), + ); + + canvas.drawRRect(body, stroke); + + // ── kijelző ────────────────────────────────────────── + final display = RRect.fromRectAndRadius( + const Rect.fromLTRB(40, 56, 60, 66), + const Radius.circular(2), + ); + + canvas.drawRRect(display, thinStroke); + + // ── felső kis jelzőcsík ────────────────────────────── + canvas.drawLine( + const Offset(43, 34), + const Offset(57, 34), + thinStroke, + ); + + // ── oldalsó gombok / státusz LED-ek ───────────────── + canvas.drawLine( + const Offset(33, 57), + const Offset(33, 57), + stroke, + ); + canvas.drawLine( + const Offset(33, 64), + const Offset(33, 64), + stroke, + ); + canvas.drawLine( + const Offset(67, 57), + const Offset(67, 57), + stroke, + ); + canvas.drawLine( + const Offset(67, 64), + const Offset(67, 64), + stroke, + ); + + // ── nyak / csatlakozó ──────────────────────────────── + canvas.drawLine( + const Offset(50, 76), + const Offset(50, 89), + stroke, + ); + + // ── alsó bot / csatlakozó rövid vonallal ───────────── + final pole = RRect.fromRectAndRadius( + const Rect.fromLTRB(44, 86, 56, 96), + const Radius.circular(2), + ); + + canvas.drawRRect(pole, stroke); + + // kis gyűrű a nyakon + canvas.drawLine( + const Offset(44, 84), + const Offset(56, 84), + thinStroke, + ); + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _GnssReceiverOutlineIconPainter oldDelegate) { + return oldDelegate.color != color || oldDelegate.strokeWidth != strokeWidth; + } +} diff --git a/lib/widgets/appbar/shell_map_appbar.dart b/lib/widgets/appbar/shell_map_appbar.dart index b8803c5..69c947c 100644 --- a/lib/widgets/appbar/shell_map_appbar.dart +++ b/lib/widgets/appbar/shell_map_appbar.dart @@ -12,8 +12,11 @@ import 'package:terepi_seged/services/ntrip_service.dart'; import 'package:terepi_seged/widgets/appbar/gnss_status_strip.dart'; import 'package:terepi_seged/widgets/gnss_status_chip.dart'; import 'package:terepi_seged/widgets/map_mode_menu_anchor.dart'; +import 'package:terepi_seged/widgets/tracking/track_recording_action.dart'; import '../tracking/tracking_sheet.dart'; +import 'gnss_receiver_icon.dart'; +import 'gnss_receiver_outline_icon.dart'; class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget { final MapSurveyController controller; @@ -32,6 +35,7 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget { toolbarHeight: 50, automaticallyImplyLeading: false, //leadingWidth: 44, + actionsPadding: EdgeInsets.zero, titleSpacing: 0, leading: Builder(builder: (context) { return IconButton( @@ -110,19 +114,45 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget { ]), actions: [ const GnssIconStatusChip(), + // Obx(() { + // final isRec = TrackingController.to.isRecording.value; + // return Badge( + // isLabelVisible: isRec, + // label: null, // csak piros pont, szám nélkül + // backgroundColor: Colors.red, + // child: IconButton( + // icon: const Icon(Icons.route_outlined), + // tooltip: 'Nyomvonal', + // onPressed: () => _openTrackingSheet(context), + // ), + // ); + // }), Obx(() { - final isRec = TrackingController.to.isRecording.value; - return Badge( - isLabelVisible: isRec, - label: null, // csak piros pont, szám nélkül - backgroundColor: Colors.red, - child: IconButton( - icon: const Icon(Icons.route_outlined), - tooltip: 'Nyomvonal', - onPressed: () => _openTrackingSheet(context), + final connected = controller.gpsIsConnected; + + return IconButton( + tooltip: connected + ? 'GNSS vevő csatlakozva' + : 'GNSS vevő nincs csatlakoztatva', + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 40, + minHeight: 44, ), + icon: GnssReceiverIcon( + size: 24, + color: connected + ? Colors.green.shade700 + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + onPressed: () {}, ); }), + TrackRecordingAction( + controller: TrackingController.to, + onTap: () => _openTrackingSheet(context), + ), PopupMenuButton( tooltip: 'További funkciók', icon: const Icon(Icons.more_vert), diff --git a/lib/widgets/map_info_card_column.dart b/lib/widgets/map_info_card_column.dart index 6b6e970..853f61b 100644 --- a/lib/widgets/map_info_card_column.dart +++ b/lib/widgets/map_info_card_column.dart @@ -44,10 +44,13 @@ class MapInfoCardColumn extends StatelessWidget { return ConstrainedBox( constraints: BoxConstraints( - maxWidth: screenWidth - 50, maxHeight: screenHeight * 0.55), + minWidth: screenWidth - 50, + maxWidth: screenWidth - 50, + maxHeight: screenHeight * 0.55), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, children: [ for (var i = 0; i < cards.length; i++) ...[ cards[i], diff --git a/lib/widgets/tracking/elapsed_badge.dart b/lib/widgets/tracking/elapsed_badge.dart new file mode 100644 index 0000000..d71338b --- /dev/null +++ b/lib/widgets/tracking/elapsed_badge.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +class ElapsedBadge extends StatelessWidget { + final String text; + final Color color; + + const ElapsedBadge({ + required this.text, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 1.5, + ), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: color.withOpacity(0.85), + width: 0, + ), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.10), + // blurRadius: 3, + // offset: const Offset(0, 1), + // ), + // ], + ), + child: Text( + text, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: color, + fontSize: 9, + fontWeight: FontWeight.w900, + height: 1.0, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/tracking/track_info_card.dart b/lib/widgets/tracking/track_info_card.dart index 3bfb4c4..da90331 100644 --- a/lib/widgets/tracking/track_info_card.dart +++ b/lib/widgets/tracking/track_info_card.dart @@ -21,20 +21,16 @@ class TrackInfoCard extends StatelessWidget { return GestureDetector( onTap: () => _openSheet(context), - child: Container( - constraints: const BoxConstraints(minWidth: 140), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.72), - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: isRec - ? Colors.red.withOpacity(0.5) - : Colors.white.withOpacity(0.12), - ), - ), - child: isRec ? _RecordingContent(ctrl: ctrl) : _IdleContent(), - ), + child: Card( + elevation: 4, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: + Theme.of(context).colorScheme.surface.withValues(alpha: 0.92), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: isRec ? _RecordingContent(ctrl: ctrl) : _IdleContent(), + )), ); }); } @@ -87,22 +83,30 @@ class _RecordingContent extends StatelessWidget { // Statisztika sor Row( - mainAxisSize: MainAxisSize.min, + //mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _MiniStat( - icon: Icons.timer_outlined, - value: ctrl.elapsedFormatted.value, - ), - const SizedBox(width: 10), + icon: Icons.timer_outlined, + value: ctrl.elapsedFormatted.value, + label: 'Idő'), + const _Divider(), _MiniStat( - icon: Icons.route, - value: _fmtDist(ctrl.sessionDistance.value), - ), - const SizedBox(width: 10), + icon: Icons.route, + value: _fmtDist(ctrl.sessionDistance.value), + label: 'Távolság'), + const _Divider(), _MiniStat( - icon: Icons.location_on_outlined, - value: '${ctrl.livePoints.length}', + icon: Icons.speed, + label: 'Sebesség', + value: + '${ctrl.currentSpeedKmh.value.toStringAsFixed(1)} km/h', ), + _Divider(), + _MiniStat( + icon: Icons.location_on_outlined, + value: '${ctrl.livePoints.length}', + label: 'Pontok'), ], ), @@ -167,6 +171,12 @@ class _RecordingContent extends StatelessWidget { : '${(m / 1000).toStringAsFixed(2)} km'; } +class _Divider extends StatelessWidget { + const _Divider(); + @override + Widget build(BuildContext context) => + Container(height: 36, width: 1, color: Colors.grey.shade300); +} // ─── Idle állapot ───────────────────────────────────────────────────────────── class _IdleContent extends StatelessWidget { @@ -300,23 +310,24 @@ class _StatusDotState extends State<_StatusDot> class _MiniStat extends StatelessWidget { final IconData icon; + final String? label; final String value; - const _MiniStat({required this.icon, required this.value}); + const _MiniStat({required this.icon, required this.value, this.label}); @override Widget build(BuildContext context) { - return Row(mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 11, color: Colors.white54), - const SizedBox(width: 3), + return Column(mainAxisSize: MainAxisSize.min, children: [ + Icon(icon, size: 16, color: Theme.of(context).colorScheme.primary), + const SizedBox(height: 2), Text( value, style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w600, + fontSize: 15, + fontWeight: FontWeight.bold, fontFeatures: [FontFeature.tabularFigures()], ), ), + label != null ? Text(label!) : SizedBox.shrink() ]); } } diff --git a/lib/widgets/tracking/track_recording_action.dart b/lib/widgets/tracking/track_recording_action.dart new file mode 100644 index 0000000..e0497e3 --- /dev/null +++ b/lib/widgets/tracking/track_recording_action.dart @@ -0,0 +1,52 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart'; + +import 'track_recording_action_visual.dart'; + +class TrackRecordingAction extends StatelessWidget { + final TrackingController controller; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + + const TrackRecordingAction({ + super.key, + required this.controller, + this.onTap, + this.onLongPress, + }); + + @override + Widget build(BuildContext context) { + return Obx(() { + return TrackRecordingActionVisual( + isRecording: controller.isRecording.value, + isPaused: controller.isPaused.value, + elapsedText: _shortElapsedText(controller.elapsedFormatted.value), + onTap: onTap, + onLongPress: onLongPress, + ); + }); + } + + String _shortElapsedText(String value) { + // A controller most HH:MM:SS formátumot ad, pl. 00:38:37. + final parts = value.split(':'); + + if (parts.length != 3) { + return value; + } + + final h = int.tryParse(parts[0]) ?? 0; + final m = parts[1]; + final s = parts[2]; + + if (h <= 0) { + return '$m:$s'; // 38:37 + } + + return '$h:$m'; // 1:04 + } +} diff --git a/lib/widgets/tracking/track_recording_action_visual.dart b/lib/widgets/tracking/track_recording_action_visual.dart new file mode 100644 index 0000000..712be87 --- /dev/null +++ b/lib/widgets/tracking/track_recording_action_visual.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; + +import 'elapsed_badge.dart'; + +class TrackRecordingActionVisual extends StatefulWidget { + final bool isRecording; + final bool isPaused; + final String elapsedText; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + + const TrackRecordingActionVisual({ + required this.isRecording, + required this.isPaused, + required this.elapsedText, + this.onTap, + this.onLongPress, + }); + + @override + State createState() => + TrackRecordingActionVisualState(); +} + +class TrackRecordingActionVisualState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _animationController; + late final Animation _scale; + late final Animation _opacity; + + bool get _shouldPulse { + return widget.isRecording && !widget.isPaused; + } + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 3000), + ); + + _scale = Tween( + begin: 0.80, + end: 1.20, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + + _opacity = Tween( + begin: 0.72, + end: 1.0, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + + _updateAnimation(); + } + + @override + void didUpdateWidget(covariant TrackRecordingActionVisual oldWidget) { + super.didUpdateWidget(oldWidget); + _updateAnimation(); + } + + void _updateAnimation() { + if (_shouldPulse && !_animationController.isAnimating) { + _animationController.repeat(reverse: true); + return; + } + + if (!_shouldPulse && _animationController.isAnimating) { + _animationController.stop(); + _animationController.reset(); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + final Color iconColor; + final Color badgeColor; + + if (!widget.isRecording) { + iconColor = colorScheme.onSurfaceVariant; + badgeColor = colorScheme.onSurfaceVariant; + } else if (widget.isPaused) { + iconColor = Colors.orange.shade800; + badgeColor = Colors.orange.shade800; + } else { + iconColor = Colors.red.shade700; + badgeColor = Colors.red.shade700; + } + + return Tooltip( + message: _tooltipText, + child: InkResponse( + radius: 22, + onTap: widget.onTap, + onLongPress: widget.onLongPress, + child: SizedBox( + width: widget.isRecording ? 58 : 40, + height: 44, + child: Center( + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + _buildIcon(iconColor), + if (widget.isRecording) + Positioned( + right: -20, + top: -4, + child: ElapsedBadge( + text: widget.elapsedText, + color: badgeColor, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildIcon(Color iconColor) { + final icon = Icon( + Icons.route_outlined, + size: 25, + color: iconColor, + ); + + if (!_shouldPulse) { + return icon; + } + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Opacity( + opacity: _opacity.value, + child: Transform.scale( + scale: _scale.value, + child: child, + ), + ); + }, + child: icon, + ); + } + + String get _tooltipText { + if (!widget.isRecording) { + return 'Útvonal rögzítés'; + } + + if (widget.isPaused) { + return 'Útvonal rögzítés szünetel\n${widget.elapsedText}'; + } + + return 'Útvonal rögzítés folyamatban\n${widget.elapsedText}'; + } +}