From 32821dabc7771003c2c0f9ed27fda0ada35b2a54 Mon Sep 17 00:00:00 2001 From: "torok.istvan" Date: Sun, 21 Jun 2026 13:52:48 +0200 Subject: [PATCH] =?UTF-8?q?Bej=C3=A1r=C3=A1s=20m=C3=B3d:=20minden=20r?= =?UTF-8?q?=C3=B6gz=C3=ADtett=20geometria=20megjelen=C3=ADt=C3=A9se=20egy?= =?UTF-8?q?=20list=C3=A1ban.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/map_survey_controller.dart | 17 + lib/services/app_database.dart | 17 + lib/widgets/appbar/shell_map_appbar.dart | 7 +- .../map_edit_tools/note_item_list_sheet.dart | 415 ++++++++++++++++++ 4 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 lib/widgets/map_edit_tools/note_item_list_sheet.dart diff --git a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart index a420b51..770e3b5 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -46,6 +46,7 @@ import 'package:terepi_seged/services/ntrip_service.dart'; import 'package:terepi_seged/services/project_service.dart'; import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart'; +import 'package:terepi_seged/widgets/map_edit_tools/note_item_list_sheet.dart'; import 'package:terepi_seged/widgets/shared_map_widgets.dart'; import '../views/measured_points_sheet.dart'; @@ -1836,4 +1837,20 @@ class MapSurveyController extends GetxController { } showSavePointDialog(point: LatLng(lat, lon)); } + + void openNoteItemList() { + Get.bottomSheet( + DraggableScrollableSheet( + initialChildSize: 0.65, + minChildSize: 0.4, + maxChildSize: 0.92, + snap: true, + expand: false, + builder: (_, __) => const NoteItemListSheet(), + ), + isScrollControlled: true, + backgroundColor: Colors.transparent, + ignoreSafeArea: false, + ); + } } diff --git a/lib/services/app_database.dart b/lib/services/app_database.dart index 893a295..2c9303a 100644 --- a/lib/services/app_database.dart +++ b/lib/services/app_database.dart @@ -549,6 +549,14 @@ class AppDatabase { where: 'note_item_id = ?', whereArgs: [noteItemId]); } + Future countNotePhotos(int noteItemId) async { + final db = await database; + final res = await db.rawQuery( + 'SELECT COUNT(*) FROM note_item_photos WHERE note_item_id = ?', + [noteItemId], + ); + return Sqflite.firstIntValue(res) ?? 0; + } // -------------- NoteItemAudio Future insertNoteAudio(NoteItemAudio audio) async { @@ -578,6 +586,15 @@ class AppDatabase { return rows.map(NoteItemAudio.fromMap).toList(); } + Future countNoteAudios(int noteItemId) async { + final db = await database; + final res = await db.rawQuery( + 'SELECT COUNT(*) FROM note_item_audios WHERE note_item_id = ?', + [noteItemId], + ); + return Sqflite.firstIntValue(res) ?? 0; + } + // ----------- Layer meta adatok Future insertImportedLayer(ImportedLayerMeta meta) async { final db = await database; diff --git a/lib/widgets/appbar/shell_map_appbar.dart b/lib/widgets/appbar/shell_map_appbar.dart index 0fb77bc..61f8a66 100644 --- a/lib/widgets/appbar/shell_map_appbar.dart +++ b/lib/widgets/appbar/shell_map_appbar.dart @@ -160,11 +160,14 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget { if (value == 1) { controller.openLayerPanel(); } + if (value == 0) { + controller.openNoteItemList(); + } }, itemBuilder: (context) => const [ PopupMenuItem( - value: 1, - child: Text('Koordináták'), + value: 0, + child: Text('Feljegyzések'), ), PopupMenuItem( value: 1, diff --git a/lib/widgets/map_edit_tools/note_item_list_sheet.dart b/lib/widgets/map_edit_tools/note_item_list_sheet.dart new file mode 100644 index 0000000..e751191 --- /dev/null +++ b/lib/widgets/map_edit_tools/note_item_list_sheet.dart @@ -0,0 +1,415 @@ +// Rögzített geometriák listája — pont, vonal, terület csoportosítva +// Minden elemnél: ikon, felirat, idő, pont szám, fotó/hang darabszám, törlés + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; + +import '../../enums/note_type.dart'; +import '../../models/note_item.dart'; +import '../../pages/map_survey/presentations/controllers/map_survey_controller.dart'; +import '../../services/app_database.dart'; +import '../../services/project_service.dart'; + +// ─── Összesített adatok egy elemhez ────────────────────────────────────────── + +class _NoteRow { + final NoteItem item; + final int photoCount; + final int audioCount; + const _NoteRow(this.item, this.photoCount, this.audioCount); +} + +// ─── Fő widget ──────────────────────────────────────────────────────────────── + +class NoteItemListSheet extends StatefulWidget { + const NoteItemListSheet({super.key}); + + @override + State createState() => _NoteItemListSheetState(); +} + +class _NoteItemListSheetState extends State { + List<_NoteRow> _points = []; + List<_NoteRow> _lines = []; + List<_NoteRow> _polygons = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final projectId = ProjectService.to.activeProjectId; + final items = await AppDatabase.instance.listNoteItems(projectId); + + final rows = await Future.wait( + items.map((item) async { + final photoCount = await AppDatabase.instance.countNotePhotos(item.id!); + final audioCount = await AppDatabase.instance.countNoteAudios(item.id!); + return _NoteRow(item, photoCount, audioCount); + }), + ); + + if (!mounted) return; + setState(() { + _points = rows.where((r) => r.item.type == NoteType.point).toList(); + _lines = rows.where((r) => r.item.type == NoteType.line).toList(); + _polygons = rows.where((r) => r.item.type == NoteType.polygon).toList(); + _loading = false; + }); + } + + Future _delete(_NoteRow row) async { + final ok = await Get.dialog(AlertDialog( + title: const Text('Geometria törlése'), + content: Text( + '"${row.item.label.isEmpty ? _typeName(row.item.type) : row.item.label}" törlése visszavonhatatlan.'), + actions: [ + TextButton(onPressed: Get.back, child: const Text('Mégse')), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: Colors.red), + onPressed: () => Get.back(result: true), + child: const Text('Törlés'), + ), + ], + )); + if (ok != true) return; + + // Controller törli az adatbázisból + a térkép rétegről + await Get.find().deleteNoteItem(row.item); + + setState(() { + _points.removeWhere((r) => r.item.id == row.item.id); + _lines.removeWhere((r) => r.item.id == row.item.id); + _polygons.removeWhere((r) => r.item.id == row.item.id); + }); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final total = _points.length + _lines.length + _polygons.length; + + return Container( + decoration: BoxDecoration( + color: cs.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 16, + offset: const Offset(0, -4)), + ], + ), + child: Column(children: [ + // ── Handle ────────────────────────────────────────────────── + Center( + child: Container( + margin: const EdgeInsets.symmetric(vertical: 10), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2)), + ), + ), + + // ── Fejléc ────────────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 10), + child: Row(children: [ + const Icon(Icons.layers_outlined, size: 20), + const SizedBox(width: 8), + const Text('Rögzített geometriák', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), + const SizedBox(width: 6), + if (!_loading) + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration( + color: cs.primaryContainer, + borderRadius: BorderRadius.circular(10), + ), + child: Text('$total', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: cs.onPrimaryContainer)), + ), + ]), + ), + + const Divider(height: 1), + + // ── Lista ──────────────────────────────────────────────────── + Expanded( + child: _loading + ? const Center(child: CircularProgressIndicator()) + : total == 0 + ? _emptyState() + : ListView( + padding: const EdgeInsets.only(bottom: 24), + children: [ + if (_points.isNotEmpty) + _Section( + icon: Icons.location_on, + color: Colors.orange, + title: 'Pontok', + rows: _points, + onDelete: _delete, + ), + if (_lines.isNotEmpty) + _Section( + icon: Icons.show_chart, + color: Colors.blue, + title: 'Vonalak', + rows: _lines, + onDelete: _delete, + ), + if (_polygons.isNotEmpty) + _Section( + icon: Icons.pentagon_outlined, + color: Colors.green, + title: 'Területek', + rows: _polygons, + onDelete: _delete, + ), + ], + ), + ), + + SizedBox(height: MediaQuery.of(context).padding.bottom + 8), + ]), + ); + } + + Widget _emptyState() => Center( + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.layers_clear_outlined, + size: 56, color: Colors.grey.shade300), + const SizedBox(height: 12), + Text('Még nincs rögzített geometria', + style: TextStyle(color: Colors.grey.shade500)), + ]), + ); + + String _typeName(NoteType t) => switch (t) { + NoteType.point => 'Pont', + NoteType.line => 'Vonal', + NoteType.polygon => 'Terület', + }; +} + +// ─── Szekció (típusonként) ──────────────────────────────────────────────────── + +class _Section extends StatelessWidget { + final IconData icon; + final Color color; + final String title; + final List<_NoteRow> rows; + final Future Function(_NoteRow) onDelete; + + const _Section({ + required this.icon, + required this.color, + required this.title, + required this.rows, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Szekció fejléc + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 6), + child: Row(children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 6), + Text(title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: color, + letterSpacing: 0.5)), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: color.withOpacity(0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Text('${rows.length}', + style: TextStyle( + fontSize: 11, fontWeight: FontWeight.w600, color: color)), + ), + ]), + ), + + // Elemek + ...rows.map((row) => _NoteItemTile( + row: row, + typeIcon: icon, + typeColor: color, + onDelete: () => onDelete(row), + )), + + Divider(height: 1, color: cs.outlineVariant), + ], + ); + } +} + +// ─── Egy sor ────────────────────────────────────────────────────────────────── + +class _NoteItemTile extends StatelessWidget { + final _NoteRow row; + final IconData typeIcon; + final Color typeColor; + final VoidCallback onDelete; + + static final _timeFmt = DateFormat('MM.dd HH:mm'); + + const _NoteItemTile({ + required this.row, + required this.typeIcon, + required this.typeColor, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + final item = row.item; + final cs = Theme.of(context).colorScheme; + + return Dismissible( + key: ValueKey(item.id), + direction: DismissDirection.endToStart, + confirmDismiss: (_) async { + onDelete(); + return false; + }, + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20), + child: const Icon(Icons.delete, color: Colors.white), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row(children: [ + // Geometria szín jelző + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: item.color.withOpacity(0.15), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: item.color.withOpacity(0.4)), + ), + child: Icon(typeIcon, size: 15, color: item.color), + ), + + const SizedBox(width: 10), + + // Fő tartalom + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Felirat / cím + Text( + item.label.isNotEmpty ? item.label : '(nincs felirat)', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: item.label.isEmpty ? cs.onSurfaceVariant : null, + fontStyle: item.label.isEmpty ? FontStyle.italic : null, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 3), + // Metaadatok sor + Row(children: [ + // Idő + Text( + _timeFmt.format(item.createdAt), + style: TextStyle(fontSize: 11, color: cs.onSurfaceVariant), + ), + // Pont/töréspontok száma (vonal/terület esetén) + if (item.type != NoteType.point) ...[ + const SizedBox(width: 8), + _MetaChip( + icon: Icons.radio_button_unchecked, + label: '${item.points.length} pt', + ), + ], + // Fotók + if (row.photoCount > 0) ...[ + const SizedBox(width: 6), + _MetaChip( + icon: Icons.photo_camera_outlined, + label: '${row.photoCount}', + color: Colors.blue.shade600, + ), + ], + // Hang + if (row.audioCount > 0) ...[ + const SizedBox(width: 6), + _MetaChip( + icon: Icons.mic_outlined, + label: '${row.audioCount}', + color: Colors.purple.shade400, + ), + ], + ]), + ], + ), + ), + + // Törlés gomb + IconButton( + icon: Icon(Icons.delete_outline, + size: 20, color: Colors.grey.shade400), + onPressed: onDelete, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 36, minHeight: 36), + ), + ]), + ), + ); + } +} + +// ─── Meta chip (pont szám, fotó, hang) ─────────────────────────────────────── + +class _MetaChip extends StatelessWidget { + final IconData icon; + final String label; + final Color? color; + const _MetaChip({ + required this.icon, + required this.label, + this.color, + }); + + @override + Widget build(BuildContext context) { + final c = color ?? Theme.of(context).colorScheme.onSurfaceVariant; + return Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(icon, size: 11, color: c), + const SizedBox(width: 2), + Text(label, + style: + TextStyle(fontSize: 11, color: c, fontWeight: FontWeight.w500)), + ]); + } +}