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:get/get.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/services/app_database.dart';
|
||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||
@ -26,6 +27,7 @@ Future<void> main() async {
|
||||
Get.put(GnssDeviceService());
|
||||
Get.put(GnssService());
|
||||
Get.put(NtripService());
|
||||
Get.put(TrackingController(), permanent: true);
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
@ -58,6 +58,7 @@ enum TrackStatus { recording, paused, finished }
|
||||
|
||||
class Track {
|
||||
final int? id;
|
||||
final int? projectId;
|
||||
final String name;
|
||||
final DateTime startTime;
|
||||
final DateTime? endTime;
|
||||
@ -70,6 +71,7 @@ class Track {
|
||||
|
||||
const Track({
|
||||
this.id,
|
||||
this.projectId,
|
||||
required this.name,
|
||||
required this.startTime,
|
||||
this.endTime,
|
||||
@ -81,6 +83,7 @@ class Track {
|
||||
|
||||
Track copyWith({
|
||||
int? id,
|
||||
int? projectId,
|
||||
String? name,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
@ -91,6 +94,7 @@ class Track {
|
||||
}) =>
|
||||
Track(
|
||||
id: id ?? this.id,
|
||||
projectId: projectId ?? this.projectId,
|
||||
name: name ?? this.name,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
@ -120,25 +124,30 @@ class Track {
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
if (id != null) 'id': id,
|
||||
if (projectId != null) 'project_id': projectId,
|
||||
'name': name,
|
||||
'start_time': startTime.toIso8601String(),
|
||||
'end_time': endTime?.toIso8601String(),
|
||||
'status': status.name,
|
||||
'source': source,
|
||||
'distance_meters': distanceMeters,
|
||||
'distance_m': distanceMeters,
|
||||
'point_count': pointCount,
|
||||
};
|
||||
|
||||
factory Track.fromMap(Map<String, dynamic> m) => Track(
|
||||
id: m['id'] as int?,
|
||||
projectId: m['project_id'] as int?,
|
||||
name: m['name'] as String,
|
||||
startTime: DateTime.parse(m['start_time'] as String),
|
||||
endTime: m['end_time'] != null
|
||||
? DateTime.parse(m['end_time'] as String)
|
||||
: 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',
|
||||
distanceMeters: (m['distance_meters'] as num?)?.toDouble() ?? 0,
|
||||
distanceMeters: (m['distance_m'] as num?)?.toDouble() ?? 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/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/tracking/presentation/controllers/tracking_controller.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_device_service.dart';
|
||||
@ -285,6 +286,18 @@ class MapSurveyController extends GetxController {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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.
|
||||
}
|
||||
|
||||
String get currentModeLabel {
|
||||
switch (mode.value) {
|
||||
case MapSurveyMode.browse:
|
||||
return 'Térkép';
|
||||
case MapSurveyMode.measure:
|
||||
return 'Bemérés';
|
||||
case MapSurveyMode.stakeout:
|
||||
return 'Kitűzés';
|
||||
case MapSurveyMode.fieldWalk:
|
||||
return 'Bejárás';
|
||||
}
|
||||
}
|
||||
String get currentModeLabel => switch (mode.value) {
|
||||
MapSurveyMode.browse => 'Térkép',
|
||||
MapSurveyMode.measure => 'Bemérés',
|
||||
MapSurveyMode.stakeout => 'Kitűzés',
|
||||
MapSurveyMode.fieldWalk => 'Bejárás',
|
||||
MapSurveyMode.track => 'Útvonal'
|
||||
};
|
||||
|
||||
IconData get currentModeIcon {
|
||||
switch (mode.value) {
|
||||
case MapSurveyMode.browse:
|
||||
return Icons.map;
|
||||
case MapSurveyMode.measure:
|
||||
return Icons.add_location_alt;
|
||||
case MapSurveyMode.stakeout:
|
||||
return Icons.gps_fixed;
|
||||
case MapSurveyMode.fieldWalk:
|
||||
return Icons.hiking;
|
||||
}
|
||||
}
|
||||
IconData get currentModeIcon => switch (mode.value) {
|
||||
MapSurveyMode.browse => Icons.map,
|
||||
MapSurveyMode.measure => Icons.add_location_alt,
|
||||
MapSurveyMode.stakeout => Icons.gps_fixed,
|
||||
MapSurveyMode.fieldWalk => Icons.hiking,
|
||||
MapSurveyMode.track => Icons.route
|
||||
};
|
||||
|
||||
void openNtripsettings() {
|
||||
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/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/tracking/presentation/controllers/tracking_controller.dart';
|
||||
import 'package:terepi_seged/utils/rive_utils.dart';
|
||||
import 'package:terepi_seged/widgets/coordinate_panel.dart';
|
||||
import 'package:terepi_seged/widgets/map_bottom_panel.dart';
|
||||
@ -29,7 +30,18 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
||||
onCenterOnGps: controller.isMapMoveToCenter,
|
||||
layers: [
|
||||
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(
|
||||
@ -78,6 +90,23 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
||||
// 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> {
|
||||
|
||||
@ -37,6 +37,8 @@ class _TrackingTaskHandler extends TaskHandler {
|
||||
// ─── TrackingController ──────────────────────────────────────────────────────
|
||||
|
||||
class TrackingController extends GetxController {
|
||||
static TrackingController get to => Get.find();
|
||||
|
||||
// ── Állapot ────────────────────────────────────────────────────────────────
|
||||
final isRecording = false.obs;
|
||||
final isPaused = false.obs;
|
||||
@ -60,6 +62,14 @@ class TrackingController extends GetxController {
|
||||
// ── Mentett track-ek listája ────────────────────────────────────────────────
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
LocationSource? _source;
|
||||
StreamSubscription<SourcePosition>? _positionSub;
|
||||
@ -188,6 +198,8 @@ class TrackingController extends GetxController {
|
||||
final finished = track!.copyWith(
|
||||
status: TrackStatus.finished,
|
||||
endTime: DateTime.now(),
|
||||
distanceMeters: _accumulatedDistance,
|
||||
pointCount: livePoints.length,
|
||||
);
|
||||
await _db.updateTrack(finished);
|
||||
currentTrack.value = finished;
|
||||
@ -209,6 +221,8 @@ class TrackingController extends GetxController {
|
||||
|
||||
Future<void> deleteTrack(int id) async {
|
||||
await _db.deleteTrack(id);
|
||||
overlayTrackIds.remove(id);
|
||||
_trackCoords.remove(id);
|
||||
await loadSavedTracks();
|
||||
}
|
||||
|
||||
@ -310,4 +324,26 @@ class TrackingController extends GetxController {
|
||||
}
|
||||
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 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,
|
||||
version: 1, onCreate: _onCreate, onUpgrade: _onUpgrade);
|
||||
@ -74,7 +79,7 @@ class AppDatabase {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS tracks (
|
||||
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,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT,
|
||||
@ -248,15 +253,20 @@ class AppDatabase {
|
||||
|
||||
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('''
|
||||
try {
|
||||
await db.transaction((txn) async {
|
||||
await txn.insert('track_points', point.toMap());
|
||||
await txn.rawUpdate('''
|
||||
UPDATE tracks
|
||||
SET distance_meters = ?,
|
||||
SET distance_m = ?,
|
||||
point_count = point_count + 1
|
||||
WHERE id = ?
|
||||
''', [newDistance, point.trackId]);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
print(
|
||||
'addPoint hiba: $e - trackId=${point.trackId} dist=$newDistance');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<TrackPoint>> getPoints(int trackId) async {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:terepi_seged/services/app_database.dart';
|
||||
import '../models/track.dart';
|
||||
|
||||
/// SQLite adatbázis-réteg a nyomvonalakhoz.
|
||||
@ -8,125 +7,16 @@ class TrackDatabase {
|
||||
TrackDatabase._();
|
||||
static final instance = TrackDatabase._();
|
||||
|
||||
static Database? _db;
|
||||
|
||||
Future<Database> get database async {
|
||||
_db ??= await _open();
|
||||
return _db!;
|
||||
}
|
||||
|
||||
Future<Database> _open() async {
|
||||
final dbPath = p.join(await getDatabasesPath(), 'tracks.db');
|
||||
return openDatabase(
|
||||
dbPath,
|
||||
version: 1,
|
||||
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();
|
||||
}
|
||||
// Minden hívás az AppDatabase-re delegál
|
||||
Future<int> insertTrack(Track t) => AppDatabase.instance.insertTrack(t);
|
||||
Future<void> updateTrack(Track t) => AppDatabase.instance.updateTrack(t);
|
||||
Future<void> deleteTrack(int id) => AppDatabase.instance.deleteTrack(id);
|
||||
Future<List<Track>> listTracks() => AppDatabase.instance.listTracks();
|
||||
Future<Track?> getTrack(int id) => AppDatabase.instance.getTrack(id);
|
||||
Future<void> addPoint(TrackPoint p, double d) =>
|
||||
AppDatabase.instance.addPoint(p, d);
|
||||
Future<List<TrackPoint>> getPoints(int id) =>
|
||||
AppDatabase.instance.getPoints(id);
|
||||
Future<List<({double lat, double lon})>> getLatLons(int trackId) =>
|
||||
AppDatabase.instance.getLatLons(trackId);
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.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/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 'eov_coordinate_card.dart';
|
||||
@ -32,6 +34,10 @@ class MapInfoCardColumn extends StatelessWidget {
|
||||
cards.add(GnssQualityCard(controller: controller));
|
||||
}
|
||||
|
||||
if (controller.mode.value == MapSurveyMode.track) {
|
||||
cards.add(TrackInfoCard());
|
||||
}
|
||||
|
||||
if (cards.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@ -45,11 +45,11 @@ class MapModeMenuAnchor extends StatelessWidget {
|
||||
child: const Text('Bejárás')),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.route),
|
||||
trailingIcon: controller.mode.value == MapSurveyMode.browse
|
||||
trailingIcon: controller.mode.value == MapSurveyMode.track
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
onPressed: () => controller.setMode(MapSurveyMode.browse),
|
||||
child: const Text('Track')),
|
||||
onPressed: () => controller.setMode(MapSurveyMode.track),
|
||||
child: const Text('Útvonal')),
|
||||
],
|
||||
builder: (context, menuController, child) {
|
||||
return InkWell(
|
||||
|
||||
@ -6,11 +6,14 @@ import 'package:get/get_state_manager/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/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/ntrip_service.dart';
|
||||
import 'package:terepi_seged/widgets/gnss_status_chip.dart';
|
||||
import 'package:terepi_seged/widgets/map_mode_menu_anchor.dart';
|
||||
|
||||
import 'tracking/tracking_sheet.dart';
|
||||
|
||||
class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final MapSurveyController controller;
|
||||
|
||||
@ -105,6 +108,19 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
]);
|
||||
}),
|
||||
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(
|
||||
tooltip: 'További funkciók',
|
||||
icon: const Icon(Icons.more_vert),
|
||||
@ -128,4 +144,13 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
if (e < 1.0) return Colors.orange;
|
||||
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