Navigációs képernyőn útvonal megjelenítése supabase táblából és navigációs képernyővel történő indítás beállítások alapján
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s

This commit is contained in:
2026-10-04 22:24:31 +02:00
parent e3ff7d070c
commit f1bb0191d5
9 changed files with 1221 additions and 1 deletions
@@ -0,0 +1,325 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/models/vibro_nav_route.dart';
import 'package:terepi_seged/pages/vibro_nav/presentation/controllers/vibro_nav_routes_controller.dart';
import 'package:terepi_seged/services/vechicle_identity_service.dart';
/// Útvonalak kezelése: lista, GeoJSON-import, szerkesztés, törlés.
/// Megnyitás: `VibroNavRoutesView.open()` (a binding-et is felveszi).
class VibroNavRoutesView extends GetView<VibroNavRoutesController> {
const VibroNavRoutesView({super.key});
static void open() {
Get.to(
() => const VibroNavRoutesView(),
binding: BindingsBuilder(() {
Get.put(VibroNavRoutesController());
}),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Útvonalak')),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.upload_file),
label: const Text('GeoJSON import'),
onPressed: () async {
final n = await controller.pickAndParse();
if (n != null) _showForm();
},
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final list = controller.routes;
if (list.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Ehhez a projekthez még nincs útvonal.\n'
'A "GeoJSON import" gombbal tölthetsz fel vonalakat, '
'vagy a PC-szoftver is feltöltheti őket.',
textAlign: TextAlign.center,
),
),
);
}
return ListView.separated(
padding: const EdgeInsets.only(bottom: 88),
itemCount: list.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => _RouteTile(route: list[i]),
);
}),
);
}
static void _showForm() {
Get.bottomSheet(
const _RouteFormSheet(),
isScrollControlled: true,
);
}
}
class _RouteTile extends StatelessWidget {
final VibroNavRoute route;
const _RouteTile({required this.route});
@override
Widget build(BuildContext context) {
final c = VibroNavRoutesController.to;
final color = VibroNavRoute.colorFor(route.routeKind, route.color);
final today = DateTime.now();
final isToday = VibroNavRoutesController.sameDay(route.validDate, today);
final isPast = route.validDate != null &&
!isToday &&
route.validDate!.isBefore(DateTime(today.year, today.month, today.day));
return ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color.withOpacity(route.isActive && !isPast ? 1 : 0.35),
shape: BoxShape.circle,
),
child: const Icon(Icons.alt_route, color: Colors.white, size: 20),
),
title: Text(route.name),
isThreeLine: true,
subtitle: Text.rich(TextSpan(children: [
TextSpan(
text: isToday ? 'MA' : route.dateLabel,
style: TextStyle(
fontWeight: isToday ? FontWeight.w800 : FontWeight.w500,
color: isToday ? Colors.green.shade700 : null,
),
),
TextSpan(
text: ' · ${route.vehicleLabel} · '
'${VibroNavRoute.kinds[route.routeKind] ?? route.routeKind}\n'
'${route.lines.length} vonal, ${route.pointCount} pont'
'${isPast ? ' · lejárt' : ''}'),
])),
trailing: Switch(
value: route.isActive,
onChanged: (v) => c.toggleActive(route, v),
),
onTap: () {
c.beginEdit(route);
VibroNavRoutesView._showForm();
},
);
}
}
/// Közös űrlap: import (több útvonal egyszerre) ÉS egy útvonal szerkesztése.
/// A lap alapja Material (nem Container(color:)), hogy a ListTile/chip
/// effektek ne takarodjanak el.
class _RouteFormSheet extends StatelessWidget {
const _RouteFormSheet();
@override
Widget build(BuildContext context) {
final c = VibroNavRoutesController.to;
final editing = c.isEditing;
return Material(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
child: SafeArea(
child: Padding(
padding: EdgeInsets.fromLTRB(
16, 16, 16, 16 + MediaQuery.of(context).viewInsets.bottom),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
editing
? 'Útvonal szerkesztése'
: '${c.draftCount} útvonal importálása',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
if (editing) ...[
TextField(
controller: c.nameCtrl,
decoration: const InputDecoration(
labelText: 'Név',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
],
const _SectionTitle('Mely napon látszik?'),
Obx(() {
final d = c.formDate.value;
final isNone = d == null;
final isToday =
VibroNavRoutesController.sameDay(d, DateTime.now());
final isTomorrow = VibroNavRoutesController.sameDay(
d, DateTime.now().add(const Duration(days: 1)));
final custom = !isNone && !isToday && !isTomorrow;
return Wrap(
spacing: 8,
children: [
ChoiceChip(
label: const Text('Minden nap'),
selected: isNone,
onSelected: (_) => c.setDateNone(),
),
ChoiceChip(
label: const Text('Ma'),
selected: isToday,
onSelected: (_) => c.setDateOffset(0),
),
ChoiceChip(
label: const Text('Holnap'),
selected: isTomorrow,
onSelected: (_) => c.setDateOffset(1),
),
ChoiceChip(
avatar: const Icon(Icons.event, size: 16),
label: Text(custom
? '${d!.year}-${d.month.toString().padLeft(2, '0')}-'
'${d.day.toString().padLeft(2, '0')}'
: 'Dátum…'),
selected: custom,
onSelected: (_) => c.pickDate(context),
),
],
);
}),
const SizedBox(height: 12),
const _SectionTitle('Melyik járműnek?'),
Obx(() => Wrap(
spacing: 8,
children: [
ChoiceChip(
label: const Text('Minden jármű'),
selected: c.formVehicle.value == null,
onSelected: (_) => c.formVehicle.value = null,
),
for (final v
in VehicleIdentityService.availableVehicles)
ChoiceChip(
label: Text(v),
selected: c.formVehicle.value == v,
onSelected: (_) => c.formVehicle.value = v,
),
],
)),
const SizedBox(height: 12),
const _SectionTitle('Fajta (a térképi szín)'),
Obx(() => Wrap(
spacing: 8,
children: [
for (final e in VibroNavRoute.kinds.entries)
ChoiceChip(
avatar: CircleAvatar(
backgroundColor:
VibroNavRoute.colorFor(e.key, null),
radius: 6),
label: Text(e.value),
selected: c.formKind.value == e.key,
onSelected: (_) => c.formKind.value = e.key,
),
],
)),
const SizedBox(height: 4),
Obx(() => SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Aktív'),
value: c.formActive.value,
onChanged: (v) => c.formActive.value = v,
)),
const SizedBox(height: 8),
Obx(() => Row(
children: [
if (editing)
TextButton.icon(
icon: const Icon(Icons.delete_outline,
color: Colors.red),
label: const Text('Törlés',
style: TextStyle(color: Colors.red)),
onPressed: c.isBusy.value
? null
: () => _confirmDelete(context, c),
),
const Spacer(),
TextButton(
onPressed: c.isBusy.value ? null : () => Get.back(),
child: const Text('Mégse'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: c.isBusy.value
? null
: () async {
final ok = await c.saveForm();
if (ok) Get.back();
},
child: c.isBusy.value
? const SizedBox(
width: 18,
height: 18,
child:
CircularProgressIndicator(strokeWidth: 2),
)
: Text(editing ? 'Mentés' : 'Importálás'),
),
],
)),
],
),
),
),
),
);
}
Future<void> _confirmDelete(
BuildContext context, VibroNavRoutesController c) async {
final route = c.editingRoute;
if (route == null) return;
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Útvonal törlése'),
content: Text('Biztosan törlöd: „${route.name}"?\n'
'Minden járműről eltűnik.'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Mégse')),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Törlés')),
],
),
);
if (ok == true) {
await c.deleteRoute(route);
Get.back();
}
}
}
class _SectionTitle extends StatelessWidget {
final String text;
const _SectionTitle(this.text);
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(text,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700)),
);
}
@@ -4,7 +4,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:terepi_seged/models/vibro_nav_route.dart';
import 'package:terepi_seged/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart';
import 'package:terepi_seged/pages/vibro_nav/presentation/views/vibro_nav_routes_view.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_service.dart';
@@ -27,6 +29,11 @@ class VibroNavView extends GetView<VibroNavController> {
style: const TextStyle(fontSize: 11),
)),
actions: [
IconButton(
icon: const Icon(Icons.alt_route),
tooltip: 'Útvonalak kezelése',
onPressed: VibroNavRoutesView.open,
),
Obx(() => IconButton(
icon: Icon(controller.showReservePoints.value
? Icons.circle_outlined
@@ -181,6 +188,7 @@ class _NavMap extends StatelessWidget {
subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'],
maxNativeZoom: 18),
const _SourcePointsLayer(),
const _RoutesLayer(),
Obx(() => controller.trail.length > 1
? PolylineLayer(polylines: [
Polyline(
@@ -648,3 +656,30 @@ class _OutlinedLabel extends StatelessWidget {
);
}
}
/// A mai napra érvényes útvonalak (kerülők). Saját Obx-ben él, ezért
/// GNSS-frissítésre nem rajzolódik újra.
class _RoutesLayer extends StatelessWidget {
const _RoutesLayer();
@override
Widget build(BuildContext context) {
final c = VibroNavController.to;
return Obx(() {
final list = c.visibleRoutes;
if (list.isEmpty) return const SizedBox.shrink();
return PolylineLayer(polylines: [
for (final r in list)
for (final line in r.lines)
Polyline(
points: line,
strokeWidth: 5,
color:
VibroNavRoute.colorFor(r.routeKind, r.color).withOpacity(0.9),
borderStrokeWidth: 2,
borderColor: Colors.white,
),
]);
});
}
}