Terepbejárás oldalon vonal és terület tulajdonságainak szerkesztése, mentés

This commit is contained in:
2026-06-16 14:12:44 +02:00
parent 537897005c
commit 1276ac0610
19 changed files with 1206 additions and 17 deletions
+61
View File
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
class ColorRow extends StatelessWidget {
final MapSurveyController ctrl;
static const _palette = [
Color(0xFF6C63FF),
Color(0xFFE74C3C),
Color(0xFF27AE60),
Color(0xFFE91E8C),
Color(0xFFE67E22),
Color(0xFF3498DB),
Color(0xFF8BC34A),
];
const ColorRow({required this.ctrl});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Kitöltési szín',
style: TextStyle(fontSize: 13, color: Colors.grey.shade600)),
const SizedBox(height: 10),
Obx(() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _palette.map((color) {
final sel = ctrl.activeEditColor.value == color;
return GestureDetector(
onTap: () => ctrl.activeEditColor.value = color,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: sel
? Theme.of(context).colorScheme.onSurface
: Colors.transparent,
width: 3,
),
boxShadow: sel
? [
BoxShadow(
color: color.withOpacity(0.45),
blurRadius: 8,
spreadRadius: 2)
]
: null,
),
),
);
}).toList(),
)),
],
);
}
}
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:terepi_seged/enums/map_edit_tool.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
class LabelField extends StatefulWidget {
final MapSurveyController ctrl;
const LabelField({required this.ctrl});
@override
State<LabelField> createState() => LabelFieldState();
}
class LabelFieldState extends State<LabelField> {
late final TextEditingController _text;
@override
void initState() {
super.initState();
_text = TextEditingController(text: widget.ctrl.activeEditLabel.value);
_text.addListener(() => widget.ctrl.activeEditLabel.value = _text.text);
}
@override
void dispose() {
_text.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final hint = switch (widget.ctrl.activeEditTool.value) {
MapEditTool.point => 'Pont neve...',
MapEditTool.line => 'Vonal neve...',
MapEditTool.polygon => 'Terület neve...',
MapEditTool.none => 'Felirat...',
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Felirat',
style: TextStyle(fontSize: 13, color: Colors.grey.shade600)),
const SizedBox(height: 6),
TextField(
controller: _text,
decoration: InputDecoration(
hintText: hint,
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
),
),
],
);
}
}
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
class LabeledSlider extends StatelessWidget {
final String label;
final double value;
final double min, max;
final int? divisions;
final String display;
final Color color;
final ValueChanged<double> onChanged;
const LabeledSlider({
required this.label,
required this.value,
required this.min,
required this.max,
required this.display,
required this.color,
required this.onChanged,
this.divisions,
});
@override
Widget build(BuildContext context) => Row(children: [
SizedBox(
width: 90,
child: Text(label,
style: TextStyle(fontSize: 13, color: Colors.grey.shade600)),
),
Expanded(
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: color,
thumbColor: color,
overlayColor: color.withOpacity(0.15),
inactiveTrackColor: Colors.grey.shade200,
trackHeight: 3.0,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
),
child: Slider(
value: value.clamp(min, max),
min: min,
max: max,
divisions: divisions,
onChanged: onChanged,
),
),
),
SizedBox(
width: 48,
child: Text(display,
textAlign: TextAlign.right,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
fontFeatures: [FontFeature.tabularFigures()])),
),
]);
}
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/enums/map_edit_tool.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
import 'map_edit_line_or_polygon_drawing_content.dart';
import 'map_edit_point_drawing_content.dart';
class MapEditDrawingToolbar extends StatelessWidget {
final MapSurveyController controller;
const MapEditDrawingToolbar({
super.key,
required this.controller,
});
@override
Widget build(BuildContext context) {
return Obx(() {
final tool = controller.activeEditTool.value;
if (tool == MapEditTool.none) {
return const SizedBox.shrink();
}
return SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(10, 0, 10, 10),
child: Align(
alignment: Alignment.bottomCenter,
child: Material(
elevation: 8,
color: Theme.of(context).colorScheme.surface.withOpacity(0.97),
borderRadius: BorderRadius.circular(22),
clipBehavior: Clip.antiAlias,
child: Container(
constraints: const BoxConstraints(
maxWidth: 560,
),
padding: const EdgeInsets.fromLTRB(10, 8, 10, 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
border: Border.all(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.22),
),
),
child: tool == MapEditTool.point
? PointDrawingContent(controller: controller)
: LineOrPolygonDrawingContent(controller: controller),
),
),
),
);
});
}
}
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/enums/map_edit_tool.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
import 'map_edit_small_status_chip.dart';
import 'map_edit_square_toolbar_button.dart';
class LineOrPolygonDrawingContent extends StatelessWidget {
final MapSurveyController controller;
const LineOrPolygonDrawingContent({
required this.controller,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Obx(() {
final pointCount = controller.editorPointCount.value;
final canFinish = controller.canFinishGeometry;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(
controller.activeEditToolIcon,
size: 20,
color: colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'${controller.activeEditToolTitle} · $pointCount pont',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w800,
),
),
),
SmallStatusChip(
text: _requiredPointText(controller.activeEditTool.value),
color: canFinish
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
],
),
const SizedBox(height: 5),
Align(
alignment: Alignment.centerLeft,
child: Text(
controller.activeEditToolHint,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.15,
),
),
),
const SizedBox(height: 8),
Row(
children: [
// SquareToolbarButton(
// tooltip: 'Utolsó pont törlése',
// icon: Icons.undo,
// // onPressed: controller.canUndo ? controller.undoLastPoint : null,
// onPressed: () {},
// ),
// const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: controller.cancelEditing,
icon: const Icon(Icons.close, size: 18),
label: const Text('Mégse'),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton.icon(
onPressed: canFinish ? controller.finishGeometry : null,
icon: const Icon(Icons.check, size: 18),
label: Text(controller.finishButtonText),
),
),
],
),
],
);
});
}
String _requiredPointText(MapEditTool tool) {
switch (tool) {
case MapEditTool.line:
return 'min. 2';
case MapEditTool.polygon:
return 'min. 3';
case MapEditTool.point:
return '1 pont';
case MapEditTool.none:
return '';
}
}
}
@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
import 'map_edit_small_status_chip.dart';
class PointDrawingContent extends StatelessWidget {
final MapSurveyController controller;
const PointDrawingContent({
required this.controller,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Obx(() {
// final hasPoint = controller.draftPoints.isNotEmpty;
final hasPoint = true;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(
controller.activeEditToolIcon,
size: 20,
color: colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
hasPoint
? 'Pont kiválasztva'
: controller.activeEditToolTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w800,
),
),
),
if (hasPoint)
SmallStatusChip(
text: '1 pont',
color: colorScheme.primary,
),
],
),
const SizedBox(height: 5),
Align(
alignment: Alignment.centerLeft,
child: Text(
hasPoint
? 'A pont helye kijelölve. A mentéshez nyomd meg a Kész gombot.'
: controller.activeEditToolHint,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.15,
),
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: controller.cancelEditing,
icon: const Icon(Icons.close, size: 18),
label: const Text('Mégse'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
//onPressed: controller.addPointFromCurrentPosition,
onPressed: () {},
icon: const Icon(Icons.my_location, size: 18),
label: const Text('Saját hely'),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton.icon(
// onPressed: controller.canFinishGeometry
// ? controller.finishGeometry
// : null,
onPressed: () {},
icon: const Icon(Icons.check, size: 18),
label: const Text('Kész'),
),
),
],
),
],
);
});
}
}
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
class SmallStatusChip extends StatelessWidget {
final String text;
final Color color;
const SmallStatusChip({
required this.text,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 7,
vertical: 3,
),
decoration: BoxDecoration(
color: color.withOpacity(0.10),
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: color.withOpacity(0.55),
width: 1,
),
),
child: Text(
text,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: color,
fontWeight: FontWeight.w800,
height: 1.0,
),
),
);
}
}
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class SquareToolbarButton extends StatelessWidget {
final String tooltip;
final IconData icon;
final VoidCallback? onPressed;
const SquareToolbarButton({
required this.tooltip,
required this.icon,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: SizedBox(
width: 42,
height: 40,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(42, 40),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: onPressed,
child: Icon(
icon,
size: 20,
),
),
),
);
}
}
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/enums/map_edit_tool.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
import 'map_toolbar_action.dart';
import 'map_toolbar_divider.dart';
class MapEditCompactToolbar extends StatelessWidget {
final MapSurveyController controller;
const MapEditCompactToolbar({
super.key,
required this.controller,
});
@override
Widget build(BuildContext context) {
return Obx(() {
final activeTool = controller.activeEditTool.value;
return SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(10, 0, 10, 10),
child: Align(
alignment: Alignment.bottomCenter,
child: Material(
elevation: 8,
color: Theme.of(context).colorScheme.surface.withOpacity(0.96),
borderRadius: BorderRadius.circular(22),
clipBehavior: Clip.antiAlias,
child: Container(
constraints: const BoxConstraints(
maxWidth: 520,
),
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 6,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
border: Border.all(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.22),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ToolbarAction(
icon: Icons.add_location_alt_outlined,
label: 'Pont',
selected: activeTool == MapEditTool.point,
onTap: controller.startPointTool,
),
ToolbarAction(
icon: Icons.polyline_outlined,
label: 'Vonal',
selected: activeTool == MapEditTool.line,
onTap: controller.startLineTool,
),
ToolbarAction(
icon: Icons.border_outer_outlined,
label: 'Terület',
selected: activeTool == MapEditTool.polygon,
onTap: controller.startPolygonTool,
),
const ToolbarDivider(),
ToolbarAction(
icon: Icons.list_alt_outlined,
label: 'Lista',
selected: false,
onTap: controller.openFeatureList,
),
ToolbarAction(
icon: Icons.layers_outlined,
label: 'Rétegek',
selected: false,
onTap: controller.openLayerPanel,
),
],
),
),
),
),
);
});
}
}
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:terepi_seged/enums/map_edit_tool.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
import 'color_row.dart';
import 'label_field.dart';
import 'opacity_slider.dart';
import 'save_sheet_actions.dart';
import 'sheet_handle.dart';
import 'stroke_slider.dart';
class MapFeatureSaveSheet extends StatelessWidget {
final MapSurveyController ctrl;
final ScrollController scrollCtrl;
const MapFeatureSaveSheet({
required this.ctrl,
required this.scrollCtrl,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.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: CustomScrollView(
controller: scrollCtrl,
slivers: [
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle
SheetHandle(),
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Stílus',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
ColorRow(ctrl: ctrl),
const SizedBox(height: 18),
OpacitySlider(ctrl: ctrl),
const SizedBox(height: 10),
if (ctrl.activeEditTool.value != MapEditTool.point) ...[
StrokeSlider(ctrl: ctrl),
const SizedBox(height: 10),
],
LabelField(ctrl: ctrl),
const SizedBox(height: 24),
SaveSheetActions(ctrl: ctrl),
SizedBox(
height: MediaQuery.of(context).padding.bottom + 12),
],
),
),
],
),
),
],
),
);
}
}
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
class ToolbarAction extends StatelessWidget {
final IconData icon;
final String label;
final bool selected;
final VoidCallback onTap;
const ToolbarAction({
required this.icon,
required this.label,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final foreground = selected
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant;
final background = selected
? colorScheme.primaryContainer.withOpacity(0.90)
: Colors.transparent;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: Tooltip(
message: label,
waitDuration: const Duration(milliseconds: 500),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOut,
width: 58,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(16),
border: selected
? Border.all(
color: colorScheme.primary.withOpacity(0.35),
width: 1,
)
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 21,
color: foreground,
),
const SizedBox(height: 2),
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: foreground,
fontWeight:
selected ? FontWeight.w800 : FontWeight.w600,
height: 1.0,
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class ToolbarDivider extends StatelessWidget {
const ToolbarDivider();
@override
Widget build(BuildContext context) {
return Container(
width: 1,
height: 32,
margin: const EdgeInsets.symmetric(horizontal: 4),
color: Theme.of(context).colorScheme.outline.withOpacity(0.22),
);
}
}
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
import 'labeled_slider.dart';
class OpacitySlider extends StatelessWidget {
final MapSurveyController ctrl;
const OpacitySlider({required this.ctrl});
@override
Widget build(BuildContext context) => Obx(() => LabeledSlider(
label: 'Átlátszóság',
value: ctrl.activeEditOpacity.value,
min: 0.1,
max: 1.0,
display: '${(ctrl.activeEditOpacity.value * 100).round()}%',
color: ctrl.activeEditColor.value,
onChanged: (v) => ctrl.activeEditOpacity.value = v,
));
}
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
class SaveSheetActions extends StatelessWidget {
final MapSurveyController ctrl;
const SaveSheetActions({required this.ctrl});
@override
Widget build(BuildContext context) {
return Row(children: [
// ← Vissza — bezárja a sheet-et, folytatja a rajzolást
Expanded(
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
icon: const Icon(Icons.arrow_back, size: 18),
label: const Text('Vissza'),
onPressed: () => Navigator.pop(context),
),
),
const SizedBox(width: 12),
// Mentés — végleges mentés, mindkét sheet bezárása
Expanded(
flex: 2,
child: Obx(() => FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor: ctrl.activeEditColor.value,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
icon: const Icon(Icons.check, size: 18),
label: const Text(
'Mentés',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
),
onPressed: () async {
//Navigator.pop(context); // style sheet bezárás
Get.back();
await ctrl.finishDraft();
},
)),
),
]);
}
}
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class SheetHandle extends StatelessWidget {
@override
Widget build(BuildContext context) => 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),
),
),
),
);
}
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
import 'labeled_slider.dart';
class StrokeSlider extends StatelessWidget {
final MapSurveyController ctrl;
const StrokeSlider({required this.ctrl});
@override
Widget build(BuildContext context) => Obx(() => LabeledSlider(
label: 'Körvonal',
value: ctrl.activeEditStrokeWidth.value,
min: 0.5,
max: 10.0,
divisions: 19,
display: '${ctrl.activeEditStrokeWidth.value.toStringAsFixed(1)} px',
color: ctrl.activeEditColor.value,
onChanged: (v) => ctrl.activeEditStrokeWidth.value = v,
));
}