Tracking funkció fejlesztése

This commit is contained in:
2026-06-11 01:20:55 +02:00
parent 2364b2311c
commit 01e6105240
18 changed files with 858 additions and 162 deletions
+21
View File
@@ -0,0 +1,21 @@
// Sheet húzható csík
import 'package:flutter/material.dart';
class Handle extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Center(
child: Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.35),
borderRadius: BorderRadius.circular(2),
),
),
),
);
}
}
+103
View File
@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
import 'stat_cell.dart';
class LiveStatsPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Obx(() {
final ctrl = TrackingController.to;
final isRec = ctrl.isRecording.value;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: isRec
? Column(children: [
// Statisztika sor
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
StatCell(
icon: Icons.timer_outlined,
label: 'Idő',
value: ctrl.elapsedFormatted.value,
large: true),
StatCell(
icon: Icons.route,
label: 'Távolság',
value: _fmtDist(ctrl.sessionDistance.value)),
StatCell(
icon: Icons.location_on_outlined,
label: 'Pontok',
value: '${ctrl.livePointCount}'),
],
),
const SizedBox(height: 10),
// Vezérlők
Row(children: [
Expanded(
child: OutlinedButton.icon(
icon: Icon(
ctrl.isPaused.value ? Icons.play_arrow : Icons.pause),
label: Text(ctrl.isPaused.value ? 'Folytatás' : 'Szünet'),
onPressed: ctrl.isPaused.value
? ctrl.resumeRecording
: ctrl.pauseRecording,
),
),
const SizedBox(width: 10),
Expanded(
child: FilledButton.icon(
icon: const Icon(Icons.stop),
label: const Text('Befejezés'),
style: FilledButton.styleFrom(
backgroundColor: Colors.red.shade700),
onPressed: ctrl.stopRecording,
),
),
]),
])
: FilledButton.icon(
icon: const Icon(Icons.fiber_manual_record),
label: const Text('Rögzítés indítása'),
style: FilledButton.styleFrom(
backgroundColor: Colors.red.shade700),
onPressed: () => _showStartDialog(context),
),
);
});
}
void _showStartDialog(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();
},
child: const Text('Indítás'),
),
],
));
}
String _fmtDist(double m) => m < 1000
? '${m.toStringAsFixed(0)} m'
: '${(m / 1000).toStringAsFixed(2)} km';
}
+32
View File
@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
// Szekció fejléc (pl. "Korábbi útvonalak")
class SectionHeader extends StatelessWidget {
final String title;
final Widget? trailing;
const SectionHeader({
required this.title,
this.trailing,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 8, 4),
child: Row(children: [
Text(
title.toUpperCase(),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.grey.shade500,
letterSpacing: 0.6,
),
),
const Spacer(),
if (trailing != null) trailing!,
]),
);
}
}
+39
View File
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class StatCell extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final bool large;
const StatCell({
super.key,
required this.icon,
required this.label,
required this.value,
this.large = false,
});
@override
Widget build(BuildContext context) {
return Column(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 16, color: Colors.grey),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: large ? 20 : 15,
fontWeight: FontWeight.w700,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
Text(
label,
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
),
),
]);
}
}
+356
View File
@@ -0,0 +1,356 @@
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),
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(),
),
);
});
}
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,
),
),
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(
mainAxisSize: MainAxisSize.min,
children: [
_MiniStat(
icon: Icons.timer_outlined,
value: ctrl.elapsedFormatted.value,
),
const SizedBox(width: 10),
_MiniStat(
icon: Icons.route,
value: _fmtDist(ctrl.sessionDistance.value),
),
const SizedBox(width: 10),
_MiniStat(
icon: Icons.location_on_outlined,
value: '${ctrl.livePoints.length}',
),
],
),
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';
}
// ─── 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;
final String value;
const _MiniStat({required this.icon, required this.value});
@override
Widget build(BuildContext context) {
return Row(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 11, color: Colors.white54),
const SizedBox(width: 3),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
fontFeatures: [FontFeature.tabularFigures()],
),
),
]);
}
}
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),
),
),
);
}
}
+65
View File
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/models/track.dart';
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
class TrackListItem extends StatelessWidget {
final Track track;
const TrackListItem({
super.key,
required this.track,
});
@override
Widget build(BuildContext context) {
return Obx(() {
final ctrl = TrackingController.to;
final isOverlay = ctrl.overlayTrackIds.contains(track.id);
return ListTile(
leading: CircleAvatar(
backgroundColor: isOverlay
? Colors.blue.withOpacity(0.2)
: Theme.of(context).colorScheme.surfaceVariant,
child: Icon(Icons.route,
color: isOverlay ? Colors.blue : Colors.grey, size: 20),
),
title: Text(track.name, style: const TextStyle(fontSize: 13)),
subtitle: Text(
'${_fmtDist(track.distanceMeters)} · '
'${track.durationFormatted} · '
'${track.pointCount} pt',
style: const TextStyle(fontSize: 11),
),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
// Overlay kapcsoló
IconButton(
icon: Icon(
isOverlay ? Icons.layers : Icons.layers_outlined,
color: isOverlay ? Colors.blue : null,
size: 20,
),
tooltip: isOverlay ? 'Elrejt' : 'Mutat a térképen',
onPressed: () {
ctrl.toggleTrackOverlay(track.id!);
// Ha bekapcsolta → zárjuk be a sheet-et
// hogy lássa a térképet
if (!isOverlay) Navigator.pop(context);
},
),
// Export
IconButton(
icon: const Icon(Icons.share, size: 20),
onPressed: () => ctrl.exportTrack(track),
),
]),
);
});
}
String _fmtDist(double m) => m < 1000
? '${m.toStringAsFixed(0)} m'
: '${(m / 1000).toStringAsFixed(2)} km';
}
+70
View File
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
import 'handle.dart';
import 'live_stat_panel.dart';
import 'section_header.dart';
import 'track_list_item.dart';
class TrackingSheet extends StatelessWidget {
const TrackingSheet();
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.35, // kezdetben kis méret
minChildSize: 0.2,
maxChildSize: 0.85, // felfelé húzva nagy lista
snap: true,
snapSizes: const [0.2, 0.35, 0.85],
builder: (_, scrollCtrl) => Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 12,
offset: const Offset(0, -4),
),
],
),
child: CustomScrollView(
controller: scrollCtrl,
slivers: [
// Handle
SliverToBoxAdapter(child: Handle()),
// Élő statisztika panel
SliverToBoxAdapter(
child: LiveStatsPanel(),
),
// Mentett track-ek fejléc
SliverToBoxAdapter(
child: SectionHeader(
title: 'Korábbi útvonalak',
trailing: TextButton(
onPressed: TrackingController.to.loadSavedTracks,
child: const Text('Frissítés'),
),
),
),
// Track lista
Obx(() => SliverList.separated(
itemCount: TrackingController.to.savedTracks.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => TrackListItem(
track: TrackingController.to.savedTracks[i],
),
)),
const SliverPadding(padding: EdgeInsets.only(bottom: 20)),
],
),
),
);
}
}