2026-06-11 01:20:55 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:get/get.dart';
|
|
|
|
|
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
|
|
|
|
|
|
|
|
|
|
import 'tracking_sheet.dart';
|
|
|
|
|
|
|
|
|
|
/// Kompakt tracking státusz kártya a térkép bal felső sarkában.
|
|
|
|
|
///
|
|
|
|
|
/// Rögzítés közben: idő, távolság, pontok + szünet/stop gombok
|
|
|
|
|
/// Rögzítésen kívül: egyetlen start gomb
|
|
|
|
|
///
|
|
|
|
|
/// Mindig látható — tap → TrackingSheet (teljes lista + vezérlők)
|
|
|
|
|
class TrackInfoCard extends StatelessWidget {
|
|
|
|
|
const TrackInfoCard({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Obx(() {
|
|
|
|
|
final ctrl = TrackingController.to;
|
|
|
|
|
final isRec = ctrl.isRecording.value;
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () => _openSheet(context),
|
2026-06-16 02:06:13 +02:00
|
|
|
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(),
|
|
|
|
|
)),
|
2026-06-11 01:20:55 +02:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _openSheet(BuildContext context) {
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
context: context,
|
|
|
|
|
isScrollControlled: true,
|
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
|
builder: (_) => const TrackingSheet(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Rögzítés közben ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class _RecordingContent extends StatelessWidget {
|
|
|
|
|
final TrackingController ctrl;
|
|
|
|
|
const _RecordingContent({required this.ctrl});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Obx(() => Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// Fejléc sor — piros pont + "REC" vagy "SZÜNET"
|
|
|
|
|
Row(mainAxisSize: MainAxisSize.min, children: [
|
|
|
|
|
_StatusDot(paused: ctrl.isPaused.value),
|
|
|
|
|
const SizedBox(width: 5),
|
|
|
|
|
Text(
|
|
|
|
|
ctrl.isPaused.value ? 'SZÜNET' : 'REC',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: ctrl.isPaused.value ? Colors.orange : Colors.red,
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
letterSpacing: 0.5,
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-07-02 15:49:23 +02:00
|
|
|
const SizedBox(width: 5),
|
|
|
|
|
Obx(() {
|
|
|
|
|
if (!ctrl.gpsSignalLost.value) return const SizedBox.shrink();
|
|
|
|
|
return Row(children: [
|
|
|
|
|
Icon(Icons.gps_off, size: 12, color: Colors.orange),
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
Text('GPS-ejl elveszett',
|
|
|
|
|
style: TextStyle(color: Colors.orange, fontSize: 10))
|
|
|
|
|
]);
|
|
|
|
|
}),
|
2026-06-11 01:20:55 +02:00
|
|
|
const Spacer(),
|
|
|
|
|
// Kártya bezárása (csak megkisebbíti, nem állítja le)
|
|
|
|
|
GestureDetector(
|
|
|
|
|
onTap: () {}, // placeholder — a kártya mindig látható
|
|
|
|
|
child: Icon(Icons.unfold_less,
|
|
|
|
|
size: 14, color: Colors.white.withOpacity(0.3)),
|
|
|
|
|
),
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
|
|
|
|
|
// Statisztika sor
|
|
|
|
|
Row(
|
2026-06-16 02:06:13 +02:00
|
|
|
//mainAxisSize: MainAxisSize.min,
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
2026-06-11 01:20:55 +02:00
|
|
|
children: [
|
|
|
|
|
_MiniStat(
|
2026-06-16 02:06:13 +02:00
|
|
|
icon: Icons.timer_outlined,
|
|
|
|
|
value: ctrl.elapsedFormatted.value,
|
|
|
|
|
label: 'Idő'),
|
|
|
|
|
const _Divider(),
|
2026-06-11 01:20:55 +02:00
|
|
|
_MiniStat(
|
2026-06-16 02:06:13 +02:00
|
|
|
icon: Icons.route,
|
|
|
|
|
value: _fmtDist(ctrl.sessionDistance.value),
|
|
|
|
|
label: 'Távolság'),
|
|
|
|
|
const _Divider(),
|
2026-06-11 01:20:55 +02:00
|
|
|
_MiniStat(
|
2026-06-16 02:06:13 +02:00
|
|
|
icon: Icons.speed,
|
|
|
|
|
label: 'Sebesség',
|
|
|
|
|
value:
|
|
|
|
|
'${ctrl.currentSpeedKmh.value.toStringAsFixed(1)} km/h',
|
2026-06-11 01:20:55 +02:00
|
|
|
),
|
2026-06-16 02:06:13 +02:00
|
|
|
_Divider(),
|
|
|
|
|
_MiniStat(
|
|
|
|
|
icon: Icons.location_on_outlined,
|
|
|
|
|
value: '${ctrl.livePoints.length}',
|
|
|
|
|
label: 'Pontok'),
|
2026-06-11 01:20:55 +02:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
|
|
|
|
|
// Vezérlő gombok
|
|
|
|
|
Row(children: [
|
|
|
|
|
// Szünet / Folytatás
|
|
|
|
|
_CardButton(
|
|
|
|
|
icon: ctrl.isPaused.value ? Icons.play_arrow : Icons.pause,
|
|
|
|
|
color: ctrl.isPaused.value ? Colors.greenAccent : Colors.orange,
|
|
|
|
|
onTap: ctrl.isPaused.value
|
|
|
|
|
? ctrl.resumeRecording
|
|
|
|
|
: ctrl.pauseRecording,
|
|
|
|
|
tooltip: ctrl.isPaused.value ? 'Folytatás' : 'Szünet',
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(width: 6),
|
|
|
|
|
|
|
|
|
|
// Stop
|
|
|
|
|
_CardButton(
|
|
|
|
|
icon: Icons.stop,
|
|
|
|
|
color: Colors.red,
|
|
|
|
|
onTap: () => _confirmStop(context),
|
|
|
|
|
tooltip: 'Befejezés',
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const Spacer(),
|
|
|
|
|
|
|
|
|
|
// Nyíl → sheet megnyitás
|
|
|
|
|
Icon(Icons.keyboard_arrow_up,
|
|
|
|
|
size: 14, color: Colors.white.withOpacity(0.35)),
|
|
|
|
|
]),
|
|
|
|
|
],
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _confirmStop(BuildContext context) {
|
|
|
|
|
Get.dialog(AlertDialog(
|
|
|
|
|
title: const Text('Rögzítés befejezése'),
|
|
|
|
|
content: Obx(() => Text(
|
|
|
|
|
'${ctrl.elapsedFormatted.value} · '
|
|
|
|
|
'${_fmtDist(ctrl.sessionDistance.value)}\n'
|
|
|
|
|
'${ctrl.livePoints.length} pont',
|
|
|
|
|
)),
|
|
|
|
|
actions: [
|
|
|
|
|
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
|
|
|
|
FilledButton(
|
|
|
|
|
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
Get.back();
|
|
|
|
|
ctrl.stopRecording();
|
|
|
|
|
},
|
|
|
|
|
child: const Text('Befejezés'),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String _fmtDist(double m) => m < 1000
|
|
|
|
|
? '${m.toStringAsFixed(0)} m'
|
|
|
|
|
: '${(m / 1000).toStringAsFixed(2)} km';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 02:06:13 +02:00
|
|
|
class _Divider extends StatelessWidget {
|
|
|
|
|
const _Divider();
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) =>
|
|
|
|
|
Container(height: 36, width: 1, color: Colors.grey.shade300);
|
|
|
|
|
}
|
2026-06-11 01:20:55 +02:00
|
|
|
// ─── Idle állapot ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class _IdleContent extends StatelessWidget {
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.route_outlined,
|
|
|
|
|
size: 14, color: Colors.white.withOpacity(0.5)),
|
|
|
|
|
const SizedBox(width: 6),
|
|
|
|
|
Text(
|
|
|
|
|
'Track',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: Colors.white.withOpacity(0.55),
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
// Indítás gomb
|
|
|
|
|
GestureDetector(
|
|
|
|
|
onTap: () => _startRecording(context),
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.red.withOpacity(0.2),
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
|
|
|
|
border: Border.all(color: Colors.red.withOpacity(0.4)),
|
|
|
|
|
),
|
|
|
|
|
child: const Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.fiber_manual_record, size: 10, color: Colors.red),
|
|
|
|
|
SizedBox(width: 4),
|
|
|
|
|
Text('Start',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: Colors.red,
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
fontWeight: FontWeight.w600)),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _startRecording(BuildContext context) {
|
|
|
|
|
final nameCtrl = TextEditingController(
|
|
|
|
|
text: 'Track ${DateTime.now().toString().substring(5, 16)}',
|
|
|
|
|
);
|
|
|
|
|
Get.dialog(AlertDialog(
|
|
|
|
|
title: const Text('Rögzítés indítása'),
|
|
|
|
|
content: TextField(
|
|
|
|
|
controller: nameCtrl,
|
|
|
|
|
autofocus: true,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: 'Track neve',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
actions: [
|
|
|
|
|
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
|
|
|
|
FilledButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
Get.back();
|
|
|
|
|
TrackingController.to.startRecording();
|
|
|
|
|
// .startRecording(name: nameCtrl.text.trim());
|
|
|
|
|
},
|
|
|
|
|
child: const Text('Indítás'),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Segéd widgetek ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class _StatusDot extends StatefulWidget {
|
|
|
|
|
final bool paused;
|
|
|
|
|
const _StatusDot({required this.paused});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<_StatusDot> createState() => _StatusDotState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _StatusDotState extends State<_StatusDot>
|
|
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
|
late AnimationController _ctrl;
|
|
|
|
|
late Animation<double> _anim;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_ctrl = AnimationController(
|
|
|
|
|
vsync: this,
|
|
|
|
|
duration: const Duration(milliseconds: 700),
|
|
|
|
|
)..repeat(reverse: true);
|
|
|
|
|
_anim = Tween(begin: 0.3, end: 1.0)
|
|
|
|
|
.animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void didUpdateWidget(_StatusDot old) {
|
|
|
|
|
super.didUpdateWidget(old);
|
|
|
|
|
widget.paused ? _ctrl.stop() : _ctrl.repeat(reverse: true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_ctrl.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final color = widget.paused ? Colors.orange : Colors.red;
|
|
|
|
|
return AnimatedBuilder(
|
|
|
|
|
animation: _anim,
|
|
|
|
|
builder: (_, __) => Opacity(
|
|
|
|
|
opacity: widget.paused ? 0.6 : _anim.value,
|
|
|
|
|
child: Container(
|
|
|
|
|
width: 7,
|
|
|
|
|
height: 7,
|
|
|
|
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _MiniStat extends StatelessWidget {
|
|
|
|
|
final IconData icon;
|
2026-06-16 02:06:13 +02:00
|
|
|
final String? label;
|
2026-06-11 01:20:55 +02:00
|
|
|
final String value;
|
2026-06-16 02:06:13 +02:00
|
|
|
const _MiniStat({required this.icon, required this.value, this.label});
|
2026-06-11 01:20:55 +02:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-06-16 02:06:13 +02:00
|
|
|
return Column(mainAxisSize: MainAxisSize.min, children: [
|
|
|
|
|
Icon(icon, size: 16, color: Theme.of(context).colorScheme.primary),
|
|
|
|
|
const SizedBox(height: 2),
|
2026-06-11 01:20:55 +02:00
|
|
|
Text(
|
|
|
|
|
value,
|
|
|
|
|
style: const TextStyle(
|
2026-06-16 02:06:13 +02:00
|
|
|
fontSize: 15,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
2026-06-11 01:20:55 +02:00
|
|
|
fontFeatures: [FontFeature.tabularFigures()],
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-06-16 02:06:13 +02:00
|
|
|
label != null ? Text(label!) : SizedBox.shrink()
|
2026-06-11 01:20:55 +02:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _CardButton extends StatelessWidget {
|
|
|
|
|
final IconData icon;
|
|
|
|
|
final Color color;
|
|
|
|
|
final VoidCallback onTap;
|
|
|
|
|
final String tooltip;
|
|
|
|
|
|
|
|
|
|
const _CardButton({
|
|
|
|
|
required this.icon,
|
|
|
|
|
required this.color,
|
|
|
|
|
required this.onTap,
|
|
|
|
|
required this.tooltip,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Tooltip(
|
|
|
|
|
message: tooltip,
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
onTap: onTap,
|
|
|
|
|
child: Container(
|
|
|
|
|
width: 28,
|
|
|
|
|
height: 24,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: color.withOpacity(0.18),
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
|
|
|
|
border: Border.all(color: color.withOpacity(0.45)),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(icon, size: 14, color: color),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|