Tracking funkció fejlesztése
This commit is contained in:
parent
2364b2311c
commit
01e6105240
@ -1 +1 @@
|
|||||||
enum MapSurveyMode { browse, measure, stakeout, fieldWalk }
|
enum MapSurveyMode { browse, measure, stakeout, fieldWalk, track }
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
|
||||||
import 'package:terepi_seged/routes/app_pages.dart';
|
import 'package:terepi_seged/routes/app_pages.dart';
|
||||||
import 'package:terepi_seged/services/app_database.dart';
|
import 'package:terepi_seged/services/app_database.dart';
|
||||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||||
@ -26,6 +27,7 @@ Future<void> main() async {
|
|||||||
Get.put(GnssDeviceService());
|
Get.put(GnssDeviceService());
|
||||||
Get.put(GnssService());
|
Get.put(GnssService());
|
||||||
Get.put(NtripService());
|
Get.put(NtripService());
|
||||||
|
Get.put(TrackingController(), permanent: true);
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,7 @@ enum TrackStatus { recording, paused, finished }
|
|||||||
|
|
||||||
class Track {
|
class Track {
|
||||||
final int? id;
|
final int? id;
|
||||||
|
final int? projectId;
|
||||||
final String name;
|
final String name;
|
||||||
final DateTime startTime;
|
final DateTime startTime;
|
||||||
final DateTime? endTime;
|
final DateTime? endTime;
|
||||||
@ -70,6 +71,7 @@ class Track {
|
|||||||
|
|
||||||
const Track({
|
const Track({
|
||||||
this.id,
|
this.id,
|
||||||
|
this.projectId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.startTime,
|
required this.startTime,
|
||||||
this.endTime,
|
this.endTime,
|
||||||
@ -81,6 +83,7 @@ class Track {
|
|||||||
|
|
||||||
Track copyWith({
|
Track copyWith({
|
||||||
int? id,
|
int? id,
|
||||||
|
int? projectId,
|
||||||
String? name,
|
String? name,
|
||||||
DateTime? startTime,
|
DateTime? startTime,
|
||||||
DateTime? endTime,
|
DateTime? endTime,
|
||||||
@ -91,6 +94,7 @@ class Track {
|
|||||||
}) =>
|
}) =>
|
||||||
Track(
|
Track(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
projectId: projectId ?? this.projectId,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
startTime: startTime ?? this.startTime,
|
startTime: startTime ?? this.startTime,
|
||||||
endTime: endTime ?? this.endTime,
|
endTime: endTime ?? this.endTime,
|
||||||
@ -120,25 +124,30 @@ class Track {
|
|||||||
|
|
||||||
Map<String, dynamic> toMap() => {
|
Map<String, dynamic> toMap() => {
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
|
if (projectId != null) 'project_id': projectId,
|
||||||
'name': name,
|
'name': name,
|
||||||
'start_time': startTime.toIso8601String(),
|
'start_time': startTime.toIso8601String(),
|
||||||
'end_time': endTime?.toIso8601String(),
|
'end_time': endTime?.toIso8601String(),
|
||||||
'status': status.name,
|
'status': status.name,
|
||||||
'source': source,
|
'source': source,
|
||||||
'distance_meters': distanceMeters,
|
'distance_m': distanceMeters,
|
||||||
'point_count': pointCount,
|
'point_count': pointCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory Track.fromMap(Map<String, dynamic> m) => Track(
|
factory Track.fromMap(Map<String, dynamic> m) => Track(
|
||||||
id: m['id'] as int?,
|
id: m['id'] as int?,
|
||||||
|
projectId: m['project_id'] as int?,
|
||||||
name: m['name'] as String,
|
name: m['name'] as String,
|
||||||
startTime: DateTime.parse(m['start_time'] as String),
|
startTime: DateTime.parse(m['start_time'] as String),
|
||||||
endTime: m['end_time'] != null
|
endTime: m['end_time'] != null
|
||||||
? DateTime.parse(m['end_time'] as String)
|
? DateTime.parse(m['end_time'] as String)
|
||||||
: null,
|
: null,
|
||||||
status: TrackStatus.values.byName(m['status'] as String),
|
status: TrackStatus.values.firstWhere(
|
||||||
|
(s) => s.name == (m['status'] as String?),
|
||||||
|
orElse: () => TrackStatus.finished,
|
||||||
|
),
|
||||||
source: m['source'] as String? ?? 'Telefon GPS',
|
source: m['source'] as String? ?? 'Telefon GPS',
|
||||||
distanceMeters: (m['distance_meters'] as num?)?.toDouble() ?? 0,
|
distanceMeters: (m['distance_m'] as num?)?.toDouble() ?? 0,
|
||||||
pointCount: m['point_count'] as int? ?? 0,
|
pointCount: m['point_count'] as int? ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:terepi_seged/pages/map_survey/presentations/views/measured_points_table_dialog.dart';
|
import 'package:terepi_seged/pages/map_survey/presentations/views/measured_points_table_dialog.dart';
|
||||||
import 'package:terepi_seged/pages/ntrip_settings/presentation/controllers/ntrip_settings_controller.dart';
|
import 'package:terepi_seged/pages/ntrip_settings/presentation/controllers/ntrip_settings_controller.dart';
|
||||||
import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart';
|
import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart';
|
||||||
|
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
|
||||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||||
import 'package:terepi_seged/services/gnss/gnss_connection.dart';
|
import 'package:terepi_seged/services/gnss/gnss_connection.dart';
|
||||||
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
||||||
@ -285,6 +286,18 @@ class MapSurveyController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setMode(MapSurveyMode newMode) {
|
void setMode(MapSurveyMode newMode) {
|
||||||
|
if (mode.value == MapSurveyMode.track &&
|
||||||
|
newMode != MapSurveyMode.track &&
|
||||||
|
TrackingController.to.isRecording.value) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Rögzítés folytatódik',
|
||||||
|
'A track rögzítés a háttérben aktív marad.',
|
||||||
|
icon: const Icon(Icons.fiber_manual_record, color: Colors.red),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
mode.value = newMode;
|
mode.value = newMode;
|
||||||
|
|
||||||
// Itt lehet módhoz kötött állapotokat állítani:
|
// Itt lehet módhoz kötött állapotokat állítani:
|
||||||
@ -294,31 +307,21 @@ class MapSurveyController extends GetxController {
|
|||||||
// - track indítás/leállítás figyelmeztetés stb.
|
// - track indítás/leállítás figyelmeztetés stb.
|
||||||
}
|
}
|
||||||
|
|
||||||
String get currentModeLabel {
|
String get currentModeLabel => switch (mode.value) {
|
||||||
switch (mode.value) {
|
MapSurveyMode.browse => 'Térkép',
|
||||||
case MapSurveyMode.browse:
|
MapSurveyMode.measure => 'Bemérés',
|
||||||
return 'Térkép';
|
MapSurveyMode.stakeout => 'Kitűzés',
|
||||||
case MapSurveyMode.measure:
|
MapSurveyMode.fieldWalk => 'Bejárás',
|
||||||
return 'Bemérés';
|
MapSurveyMode.track => 'Útvonal'
|
||||||
case MapSurveyMode.stakeout:
|
};
|
||||||
return 'Kitűzés';
|
|
||||||
case MapSurveyMode.fieldWalk:
|
|
||||||
return 'Bejárás';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData get currentModeIcon {
|
IconData get currentModeIcon => switch (mode.value) {
|
||||||
switch (mode.value) {
|
MapSurveyMode.browse => Icons.map,
|
||||||
case MapSurveyMode.browse:
|
MapSurveyMode.measure => Icons.add_location_alt,
|
||||||
return Icons.map;
|
MapSurveyMode.stakeout => Icons.gps_fixed,
|
||||||
case MapSurveyMode.measure:
|
MapSurveyMode.fieldWalk => Icons.hiking,
|
||||||
return Icons.add_location_alt;
|
MapSurveyMode.track => Icons.route
|
||||||
case MapSurveyMode.stakeout:
|
};
|
||||||
return Icons.gps_fixed;
|
|
||||||
case MapSurveyMode.fieldWalk:
|
|
||||||
return Icons.hiking;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void openNtripsettings() {
|
void openNtripsettings() {
|
||||||
if (!Get.isRegistered<NtripSettingsController>()) {
|
if (!Get.isRegistered<NtripSettingsController>()) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'package:latlong2/latlong.dart';
|
|||||||
import 'package:terepi_seged/enums/map_survey_mode.dart';
|
import 'package:terepi_seged/enums/map_survey_mode.dart';
|
||||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||||
import 'package:terepi_seged/pages/map_survey/presentations/views/settings_dialog.dart';
|
import 'package:terepi_seged/pages/map_survey/presentations/views/settings_dialog.dart';
|
||||||
|
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
|
||||||
import 'package:terepi_seged/utils/rive_utils.dart';
|
import 'package:terepi_seged/utils/rive_utils.dart';
|
||||||
import 'package:terepi_seged/widgets/coordinate_panel.dart';
|
import 'package:terepi_seged/widgets/coordinate_panel.dart';
|
||||||
import 'package:terepi_seged/widgets/map_bottom_panel.dart';
|
import 'package:terepi_seged/widgets/map_bottom_panel.dart';
|
||||||
@ -29,7 +30,18 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
|||||||
onCenterOnGps: controller.isMapMoveToCenter,
|
onCenterOnGps: controller.isMapMoveToCenter,
|
||||||
layers: [
|
layers: [
|
||||||
Obx(() =>
|
Obx(() =>
|
||||||
MarkerLayer(markers: controller.currentLocationMarker.toList()))
|
MarkerLayer(markers: controller.currentLocationMarker.toList())),
|
||||||
|
|
||||||
|
// Track polyline
|
||||||
|
Obx(() {
|
||||||
|
final isTracking = TrackingController.to.isRecording.value;
|
||||||
|
final inTrackMode = controller.mode.value == MapSurveyMode.track;
|
||||||
|
if (!isTracking && !inTrackMode) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
} else {
|
||||||
|
return _buildTrackLayer();
|
||||||
|
}
|
||||||
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -78,6 +90,23 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
|||||||
// child: MapBottomPanel(controller: controller))
|
// child: MapBottomPanel(controller: controller))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTrackLayer() {
|
||||||
|
// FutureBuilder helyett a controller livePoints-ból
|
||||||
|
return Obx(() {
|
||||||
|
final ctrl = TrackingController.to;
|
||||||
|
if (ctrl.livePoints.isEmpty) return const SizedBox.shrink();
|
||||||
|
return PolylineLayer(polylines: [
|
||||||
|
Polyline(
|
||||||
|
points: ctrl.livePoints
|
||||||
|
.map((p) => LatLng(p.latitude, p.longitude))
|
||||||
|
.toList(),
|
||||||
|
color: Colors.red.withOpacity(0.85),
|
||||||
|
strokeWidth: 3.0,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ModeSelector extends GetView<MapSurveyController> {
|
class _ModeSelector extends GetView<MapSurveyController> {
|
||||||
|
|||||||
@ -37,6 +37,8 @@ class _TrackingTaskHandler extends TaskHandler {
|
|||||||
// ─── TrackingController ──────────────────────────────────────────────────────
|
// ─── TrackingController ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
class TrackingController extends GetxController {
|
class TrackingController extends GetxController {
|
||||||
|
static TrackingController get to => Get.find();
|
||||||
|
|
||||||
// ── Állapot ────────────────────────────────────────────────────────────────
|
// ── Állapot ────────────────────────────────────────────────────────────────
|
||||||
final isRecording = false.obs;
|
final isRecording = false.obs;
|
||||||
final isPaused = false.obs;
|
final isPaused = false.obs;
|
||||||
@ -60,6 +62,14 @@ class TrackingController extends GetxController {
|
|||||||
// ── Mentett track-ek listája ────────────────────────────────────────────────
|
// ── Mentett track-ek listája ────────────────────────────────────────────────
|
||||||
final savedTracks = <Track>[].obs;
|
final savedTracks = <Track>[].obs;
|
||||||
|
|
||||||
|
// Melyik track-ek látszanak overlay-ként a térképen
|
||||||
|
final overlayTrackIds = <int>[].obs;
|
||||||
|
|
||||||
|
// Betöltött koordináták cache
|
||||||
|
final Map<int, List<LatLng>> _trackCoords = {};
|
||||||
|
|
||||||
|
int get livePointCount => livePoints.length;
|
||||||
|
|
||||||
// ── Belső állapot ──────────────────────────────────────────────────────────
|
// ── Belső állapot ──────────────────────────────────────────────────────────
|
||||||
LocationSource? _source;
|
LocationSource? _source;
|
||||||
StreamSubscription<SourcePosition>? _positionSub;
|
StreamSubscription<SourcePosition>? _positionSub;
|
||||||
@ -188,6 +198,8 @@ class TrackingController extends GetxController {
|
|||||||
final finished = track!.copyWith(
|
final finished = track!.copyWith(
|
||||||
status: TrackStatus.finished,
|
status: TrackStatus.finished,
|
||||||
endTime: DateTime.now(),
|
endTime: DateTime.now(),
|
||||||
|
distanceMeters: _accumulatedDistance,
|
||||||
|
pointCount: livePoints.length,
|
||||||
);
|
);
|
||||||
await _db.updateTrack(finished);
|
await _db.updateTrack(finished);
|
||||||
currentTrack.value = finished;
|
currentTrack.value = finished;
|
||||||
@ -209,6 +221,8 @@ class TrackingController extends GetxController {
|
|||||||
|
|
||||||
Future<void> deleteTrack(int id) async {
|
Future<void> deleteTrack(int id) async {
|
||||||
await _db.deleteTrack(id);
|
await _db.deleteTrack(id);
|
||||||
|
overlayTrackIds.remove(id);
|
||||||
|
_trackCoords.remove(id);
|
||||||
await loadSavedTracks();
|
await loadSavedTracks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,4 +324,26 @@ class TrackingController extends GetxController {
|
|||||||
}
|
}
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overlay ki/be kapcsolása — ha bekapcsol, betölti a koordinátákat
|
||||||
|
void toggleTrackOverlay(int trackId) {
|
||||||
|
if (overlayTrackIds.contains(trackId)) {
|
||||||
|
overlayTrackIds.remove(trackId);
|
||||||
|
overlayTrackIds.refresh();
|
||||||
|
} else {
|
||||||
|
overlayTrackIds.add(trackId);
|
||||||
|
overlayTrackIds.refresh();
|
||||||
|
_loadTrackCoords(trackId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Koordináták visszaadása — üres lista ha még nincs betöltve
|
||||||
|
List<LatLng> getCoordsFor(int trackId) => _trackCoords[trackId] ?? [];
|
||||||
|
|
||||||
|
Future<void> _loadTrackCoords(int trackId) async {
|
||||||
|
if (_trackCoords.containsKey(trackId)) return;
|
||||||
|
final pts = await _db.getLatLons(trackId);
|
||||||
|
_trackCoords[trackId] = pts.map((p) => LatLng(p.lat, p.lon)).toList();
|
||||||
|
overlayTrackIds.refresh(); // térkép frissítés
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,12 @@ class AppDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final directory = await getExternalStorageDirectory();
|
final directory = await getExternalStorageDirectory();
|
||||||
final path = p.join(directory!.path, 'database', 'terepi_seged.db');
|
final dbDir = Directory(p.join(directory!.path, 'database'));
|
||||||
|
|
||||||
|
if (!await dbDir.exists()) {
|
||||||
|
await dbDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
final path = p.join(dbDir.path, 'terepi_seged.db');
|
||||||
|
|
||||||
return openDatabase(path,
|
return openDatabase(path,
|
||||||
version: 1, onCreate: _onCreate, onUpgrade: _onUpgrade);
|
version: 1, onCreate: _onCreate, onUpgrade: _onUpgrade);
|
||||||
@ -74,7 +79,7 @@ class AppDatabase {
|
|||||||
await db.execute('''
|
await db.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS tracks (
|
CREATE TABLE IF NOT EXISTS tracks (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
start_time TEXT NOT NULL,
|
start_time TEXT NOT NULL,
|
||||||
end_time TEXT,
|
end_time TEXT,
|
||||||
@ -248,15 +253,20 @@ class AppDatabase {
|
|||||||
|
|
||||||
Future<void> addPoint(TrackPoint point, double newDistance) async {
|
Future<void> addPoint(TrackPoint point, double newDistance) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
|
try {
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
await txn.insert('track_points', point.toMap());
|
await txn.insert('track_points', point.toMap());
|
||||||
await txn.rawUpdate('''
|
await txn.rawUpdate('''
|
||||||
UPDATE tracks
|
UPDATE tracks
|
||||||
SET distance_meters = ?,
|
SET distance_m = ?,
|
||||||
point_count = point_count + 1
|
point_count = point_count + 1
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
''', [newDistance, point.trackId]);
|
''', [newDistance, point.trackId]);
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print(
|
||||||
|
'addPoint hiba: $e - trackId=${point.trackId} dist=$newDistance');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TrackPoint>> getPoints(int trackId) async {
|
Future<List<TrackPoint>> getPoints(int trackId) async {
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:terepi_seged/services/app_database.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import '../models/track.dart';
|
import '../models/track.dart';
|
||||||
|
|
||||||
/// SQLite adatbázis-réteg a nyomvonalakhoz.
|
/// SQLite adatbázis-réteg a nyomvonalakhoz.
|
||||||
@ -8,125 +7,16 @@ class TrackDatabase {
|
|||||||
TrackDatabase._();
|
TrackDatabase._();
|
||||||
static final instance = TrackDatabase._();
|
static final instance = TrackDatabase._();
|
||||||
|
|
||||||
static Database? _db;
|
// Minden hívás az AppDatabase-re delegál
|
||||||
|
Future<int> insertTrack(Track t) => AppDatabase.instance.insertTrack(t);
|
||||||
Future<Database> get database async {
|
Future<void> updateTrack(Track t) => AppDatabase.instance.updateTrack(t);
|
||||||
_db ??= await _open();
|
Future<void> deleteTrack(int id) => AppDatabase.instance.deleteTrack(id);
|
||||||
return _db!;
|
Future<List<Track>> listTracks() => AppDatabase.instance.listTracks();
|
||||||
}
|
Future<Track?> getTrack(int id) => AppDatabase.instance.getTrack(id);
|
||||||
|
Future<void> addPoint(TrackPoint p, double d) =>
|
||||||
Future<Database> _open() async {
|
AppDatabase.instance.addPoint(p, d);
|
||||||
final dbPath = p.join(await getDatabasesPath(), 'tracks.db');
|
Future<List<TrackPoint>> getPoints(int id) =>
|
||||||
return openDatabase(
|
AppDatabase.instance.getPoints(id);
|
||||||
dbPath,
|
Future<List<({double lat, double lon})>> getLatLons(int trackId) =>
|
||||||
version: 1,
|
AppDatabase.instance.getLatLons(trackId);
|
||||||
onCreate: _onCreate,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onCreate(Database db, int version) async {
|
|
||||||
await db.execute('''
|
|
||||||
CREATE TABLE tracks (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
start_time TEXT NOT NULL,
|
|
||||||
end_time TEXT,
|
|
||||||
status TEXT NOT NULL DEFAULT 'recording',
|
|
||||||
source TEXT NOT NULL DEFAULT 'Telefon GPS',
|
|
||||||
distance_meters REAL NOT NULL DEFAULT 0,
|
|
||||||
point_count INTEGER NOT NULL DEFAULT 0
|
|
||||||
)
|
|
||||||
''');
|
|
||||||
|
|
||||||
await db.execute('''
|
|
||||||
CREATE TABLE track_points (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
track_id INTEGER NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
|
|
||||||
latitude REAL NOT NULL,
|
|
||||||
longitude REAL NOT NULL,
|
|
||||||
altitude REAL,
|
|
||||||
accuracy REAL,
|
|
||||||
speed REAL,
|
|
||||||
heading REAL,
|
|
||||||
timestamp TEXT NOT NULL
|
|
||||||
)
|
|
||||||
''');
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
'CREATE INDEX idx_tp_track ON track_points(track_id, timestamp)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Tracks CRUD ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
Future<int> insertTrack(Track track) async {
|
|
||||||
final db = await database;
|
|
||||||
return db.insert('tracks', track.toMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateTrack(Track track) async {
|
|
||||||
final db = await database;
|
|
||||||
await db.update('tracks', track.toMap(),
|
|
||||||
where: 'id = ?', whereArgs: [track.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteTrack(int id) async {
|
|
||||||
final db = await database;
|
|
||||||
await db.delete('tracks', where: 'id = ?', whereArgs: [id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Track>> listTracks() async {
|
|
||||||
final db = await database;
|
|
||||||
final rows = await db.query('tracks', orderBy: 'start_time DESC');
|
|
||||||
return rows.map(Track.fromMap).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Track?> getTrack(int id) async {
|
|
||||||
final db = await database;
|
|
||||||
final rows =
|
|
||||||
await db.query('tracks', where: 'id = ?', whereArgs: [id], limit: 1);
|
|
||||||
return rows.isEmpty ? null : Track.fromMap(rows.first);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── TrackPoints ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Egyetlen pont hozzáadása + track statisztikák atomi frissítése.
|
|
||||||
Future<void> addPoint(TrackPoint point, double newDistance) async {
|
|
||||||
final db = await database;
|
|
||||||
await db.transaction((txn) async {
|
|
||||||
await txn.insert('track_points', point.toMap());
|
|
||||||
await txn.rawUpdate('''
|
|
||||||
UPDATE tracks
|
|
||||||
SET distance_meters = ?,
|
|
||||||
point_count = point_count + 1
|
|
||||||
WHERE id = ?
|
|
||||||
''', [newDistance, point.trackId]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<TrackPoint>> getPoints(int trackId) async {
|
|
||||||
final db = await database;
|
|
||||||
final rows = await db.query(
|
|
||||||
'track_points',
|
|
||||||
where: 'track_id = ?',
|
|
||||||
whereArgs: [trackId],
|
|
||||||
orderBy: 'timestamp ASC',
|
|
||||||
);
|
|
||||||
return rows.map(TrackPoint.fromMap).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Csak a koordinátákat adja vissza — a térkép polyline-hoz elég.
|
|
||||||
Future<List<({double lat, double lon})>> getLatLons(int trackId) async {
|
|
||||||
final db = await database;
|
|
||||||
final rows = await db.query(
|
|
||||||
'track_points',
|
|
||||||
columns: ['latitude', 'longitude'],
|
|
||||||
where: 'track_id = ?',
|
|
||||||
whereArgs: [trackId],
|
|
||||||
orderBy: 'timestamp ASC',
|
|
||||||
);
|
|
||||||
return rows
|
|
||||||
.map((r) =>
|
|
||||||
(lat: r['latitude'] as double, lon: r['longitude'] as double))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:terepi_seged/enums/map_survey_mode.dart';
|
||||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
|
||||||
import 'package:terepi_seged/widgets/gnss_quality_card.dart';
|
import 'package:terepi_seged/widgets/gnss_quality_card.dart';
|
||||||
|
import 'package:terepi_seged/widgets/tracking/track_info_card.dart';
|
||||||
import 'package:terepi_seged/widgets/wgs84_coordinate_card.dart';
|
import 'package:terepi_seged/widgets/wgs84_coordinate_card.dart';
|
||||||
|
|
||||||
import 'eov_coordinate_card.dart';
|
import 'eov_coordinate_card.dart';
|
||||||
@ -32,6 +34,10 @@ class MapInfoCardColumn extends StatelessWidget {
|
|||||||
cards.add(GnssQualityCard(controller: controller));
|
cards.add(GnssQualityCard(controller: controller));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (controller.mode.value == MapSurveyMode.track) {
|
||||||
|
cards.add(TrackInfoCard());
|
||||||
|
}
|
||||||
|
|
||||||
if (cards.isEmpty) {
|
if (cards.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,11 +45,11 @@ class MapModeMenuAnchor extends StatelessWidget {
|
|||||||
child: const Text('Bejárás')),
|
child: const Text('Bejárás')),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(Icons.route),
|
leadingIcon: const Icon(Icons.route),
|
||||||
trailingIcon: controller.mode.value == MapSurveyMode.browse
|
trailingIcon: controller.mode.value == MapSurveyMode.track
|
||||||
? const Icon(Icons.check)
|
? const Icon(Icons.check)
|
||||||
: null,
|
: null,
|
||||||
onPressed: () => controller.setMode(MapSurveyMode.browse),
|
onPressed: () => controller.setMode(MapSurveyMode.track),
|
||||||
child: const Text('Track')),
|
child: const Text('Útvonal')),
|
||||||
],
|
],
|
||||||
builder: (context, menuController, child) {
|
builder: (context, menuController, child) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
|||||||
@ -6,11 +6,14 @@ import 'package:get/get_state_manager/get_state_manager.dart';
|
|||||||
import 'package:get/state_manager.dart';
|
import 'package:get/state_manager.dart';
|
||||||
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.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 'package:terepi_seged/pages/shell/presentations/controllers/shell_controller.dart';
|
||||||
|
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
|
||||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||||
import 'package:terepi_seged/widgets/gnss_status_chip.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/map_mode_menu_anchor.dart';
|
||||||
|
|
||||||
|
import 'tracking/tracking_sheet.dart';
|
||||||
|
|
||||||
class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final MapSurveyController controller;
|
final MapSurveyController controller;
|
||||||
|
|
||||||
@ -105,6 +108,19 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
actions: [
|
actions: [
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
tooltip: 'További funkciók',
|
tooltip: 'További funkciók',
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
@ -128,4 +144,13 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
if (e < 1.0) return Colors.orange;
|
if (e < 1.0) return Colors.orange;
|
||||||
return Colors.red;
|
return Colors.red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openTrackingSheet(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (_) => const TrackingSheet(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
lib/widgets/tracking/handle.dart
Normal file
21
lib/widgets/tracking/handle.dart
Normal 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
lib/widgets/tracking/live_stat_panel.dart
Normal file
103
lib/widgets/tracking/live_stat_panel.dart
Normal 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
lib/widgets/tracking/section_header.dart
Normal file
32
lib/widgets/tracking/section_header.dart
Normal 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
lib/widgets/tracking/stat_cell.dart
Normal file
39
lib/widgets/tracking/stat_cell.dart
Normal 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
lib/widgets/tracking/track_info_card.dart
Normal file
356
lib/widgets/tracking/track_info_card.dart
Normal 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
lib/widgets/tracking/track_list_item.dart
Normal file
65
lib/widgets/tracking/track_list_item.dart
Normal 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
lib/widgets/tracking/tracking_sheet.dart
Normal file
70
lib/widgets/tracking/tracking_sheet.dart
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user