From 5ef22c7b50c7d16a53ba663ccf44bc38521a9dbd Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Jun 2026 01:43:14 +0200 Subject: [PATCH 1/9] added border radius to background --- lib/widgets/task_dismissible.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/widgets/task_dismissible.dart b/lib/widgets/task_dismissible.dart index 5df9a80..ef4d3fd 100644 --- a/lib/widgets/task_dismissible.dart +++ b/lib/widgets/task_dismissible.dart @@ -16,7 +16,10 @@ class TaskDismissible extends StatelessWidget { key: key!, direction: DismissDirection.startToEnd, background: Container( - color: Theme.of(context).colorScheme.error, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.error, + ), child: Align( alignment: AlignmentGeometry.centerLeft, child: Padding( From f1c1578620e9cd46df94df27c98d9d4a8d1db7c0 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Jun 2026 13:20:46 +0200 Subject: [PATCH 2/9] added const for formColumnSpacing --- lib/app_theme.dart | 2 ++ lib/pages/task_edit_page.dart | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/app_theme.dart b/lib/app_theme.dart index 306eaf4..96da459 100644 --- a/lib/app_theme.dart +++ b/lib/app_theme.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; class AppTheme { AppTheme._(); + static const double formColumnSpacing = 12.0; + static ThemeData get lightTheme => _baseTheme( colorScheme: ColorScheme.fromSeed( seedColor: Colors.indigo, diff --git a/lib/pages/task_edit_page.dart b/lib/pages/task_edit_page.dart index a25d716..278bacc 100644 --- a/lib/pages/task_edit_page.dart +++ b/lib/pages/task_edit_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../app_theme.dart'; import '../model/callback_models/create_task_request.dart'; import '../model/extensions/controller_context.dart'; import '../model/task.dart'; @@ -80,7 +81,7 @@ class _TaskEditPageState extends State { horizontal: MediaQuery.of(context).size.width * 0.05, ), child: Column( - spacing: 12, + spacing: AppTheme.formColumnSpacing, children: [ SizedBox(height: 6), TextFormField( From a71cc454ebdcbacc240c33f6226e95d03d5f9988 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Jun 2026 13:21:08 +0200 Subject: [PATCH 3/9] extended location and latlng models --- lib/model/latlng.dart | 14 ++++++++++++++ lib/model/location.dart | 19 +++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/model/latlng.dart b/lib/model/latlng.dart index bda28da..9a90e99 100644 --- a/lib/model/latlng.dart +++ b/lib/model/latlng.dart @@ -12,4 +12,18 @@ class LatLng { Map toJson() { return {'lat': lat, 'lng': lng}; } + + @override + bool operator ==(Object other) { + if (other is! LatLng) return false; + return hashCode == other.hashCode; + } + + @override + int get hashCode => Object.hash(lat, lng); + + @override + String toString() { + return '${lat.toString()}, ${lng.toString()}'; + } } diff --git a/lib/model/location.dart b/lib/model/location.dart index e247aea..602e1f5 100644 --- a/lib/model/location.dart +++ b/lib/model/location.dart @@ -1,19 +1,34 @@ import 'latlng.dart'; class Location { + final String name; final LatLng coordinates; final String address; - Location({required this.coordinates, this.address = ''}); + Location({required this.name, required this.coordinates, this.address = ''}); factory Location.fromJson(Map json) { return Location( + name: json['name'] as String, address: json['address'] as String, coordinates: LatLng.fromJson(json['coordinates']), ); } Map toJson() { - return {'address': address, 'coordinates': coordinates.toJson()}; + return { + 'name': name, + 'address': address, + 'coordinates': coordinates.toJson(), + }; } + + @override + bool operator ==(Object other) { + if (other is! Location) return false; + return hashCode == other.hashCode; + } + + @override + int get hashCode => coordinates.hashCode; } From e8057a8cc664a59982d86693759c2ef6d85e4dbc Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Jun 2026 13:21:23 +0200 Subject: [PATCH 4/9] added getter for locations --- lib/service/controllers/location_controller.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/service/controllers/location_controller.dart b/lib/service/controllers/location_controller.dart index ad8850a..5879ee0 100644 --- a/lib/service/controllers/location_controller.dart +++ b/lib/service/controllers/location_controller.dart @@ -12,6 +12,8 @@ class LocationController extends ChangeNotifier { final List _locations = []; + List get locations => _locations; + Future addLocation(Location location) { _locations.add(location); notifyListeners(); From 20a7c88d2f6fd97f2ebad73854dcf3bbe6b2800c Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Jun 2026 13:48:55 +0200 Subject: [PATCH 5/9] fixed warning --- lib/pages/task_edit_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/task_edit_page.dart b/lib/pages/task_edit_page.dart index 278bacc..a799715 100644 --- a/lib/pages/task_edit_page.dart +++ b/lib/pages/task_edit_page.dart @@ -168,7 +168,7 @@ class _TaskEditPageState extends State { ], ), ).then((result) { - if (result != null && result && context.mounted) { + if (result != null && result && mounted) { Navigator.of(context).pop(); } }); From 9e950a1767bae2e4dbb7fc6c88b23cbd8fd8591c Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Jun 2026 14:01:58 +0200 Subject: [PATCH 6/9] added validators --- lib/service/validators.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/service/validators.dart b/lib/service/validators.dart index 6548fc4..aec3b9e 100644 --- a/lib/service/validators.dart +++ b/lib/service/validators.dart @@ -12,3 +12,17 @@ String? dateTimeValidator(String? value) { return DateTime.tryParse(value) != null ? null : 'Not a date format'; } + +String? coordinatesValidator(String? value) { + if (value == null || value.isEmpty) return null; + + if (RegExp(r'^\d+\.?\d*, *\d+\.?\d*$').hasMatch(value)) { + return null; + } + return 'Not a valid coordinate format'; +} + +String? notEmptyValidator(String? value) { + if (value != null && value.isNotEmpty) return null; + return 'Can\'t be empty'; +} From 383de6a33b04d3ad7ee6ccdfe5e18613174ac7ad Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Jun 2026 14:02:11 +0200 Subject: [PATCH 7/9] added from string constructor --- lib/model/latlng.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/model/latlng.dart b/lib/model/latlng.dart index 9a90e99..fc7cf2a 100644 --- a/lib/model/latlng.dart +++ b/lib/model/latlng.dart @@ -5,6 +5,15 @@ class LatLng { LatLng(this.lat, this.lng); LatLng.empty() : lat = 0, lng = 0; + /// must be formatted 'double, double'. For example 35.35217, 89.19659 + factory LatLng.fromString(String latLng) { + final splitString = latLng.split(','); + final lat = double.parse(splitString[0].trim()); + final lng = double.parse(splitString[1].trim()); + + return LatLng(lat, lng); + } + factory LatLng.fromJson(Map json) { return LatLng(json['lat'] as double, json['lng'] as double); } From 9d0cb7668d57e6ec4279b26298823e4466428253 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Jun 2026 14:02:35 +0200 Subject: [PATCH 8/9] added updateLocation function --- lib/service/controllers/location_controller.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/service/controllers/location_controller.dart b/lib/service/controllers/location_controller.dart index 5879ee0..cba76c1 100644 --- a/lib/service/controllers/location_controller.dart +++ b/lib/service/controllers/location_controller.dart @@ -26,6 +26,16 @@ class LocationController extends ChangeNotifier { return _repository.deleteLocation(location); } + Future updateLocation(Location oldLocation, Location newLocation) { + final index = _locations.indexOf(oldLocation); + _locations.remove(oldLocation); + _locations.insert(index, newLocation); + notifyListeners(); + return _repository + .deleteLocation(oldLocation) + .whenComplete(() => _repository.createLocation(newLocation)); + } + Future _loadLocations() { _locations.clear(); return _repository.loadLocations().then( From 21ef9c4ab5dc8b01393794c8c03f5467e57aef87 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Jun 2026 14:03:05 +0200 Subject: [PATCH 9/9] added location overview screen --- lib/main.dart | 2 + lib/pages/locations_overview_page.dart | 74 +++++++++++++++ lib/pages/task_overview_page.dart | 21 ++++- .../dialogs/create_location_dialog.dart | 93 +++++++++++++++++++ 4 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 lib/pages/locations_overview_page.dart create mode 100644 lib/widgets/dialogs/create_location_dialog.dart diff --git a/lib/main.dart b/lib/main.dart index 4b87d97..6836f33 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'app_theme.dart'; import 'model/repositories/local_repository.dart'; +import 'pages/locations_overview_page.dart'; import 'pages/task_edit_page.dart'; import 'pages/task_overview_page.dart'; import 'service/controller_scope.dart'; @@ -39,6 +40,7 @@ class MainApp extends StatelessWidget { routes: { TaskOverviewPage.routeName: (context) => TaskOverviewPage(), TaskEditPage.routeName: (context) => TaskEditPage(), + LocationsOverviewPage.routeName: (context) => LocationsOverviewPage(), }, initialRoute: TaskOverviewPage.routeName, ); diff --git a/lib/pages/locations_overview_page.dart b/lib/pages/locations_overview_page.dart new file mode 100644 index 0000000..b1835bd --- /dev/null +++ b/lib/pages/locations_overview_page.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +import '../model/extensions/controller_context.dart'; +import '../model/location.dart'; +import '../service/controllers/location_controller.dart'; +import '../widgets/dialogs/create_location_dialog.dart'; + +class LocationsOverviewPage extends StatefulWidget { + static const routeName = '/locations'; + const LocationsOverviewPage({super.key}); + + @override + State createState() => _LocationsOverviewPageState(); +} + +class _LocationsOverviewPageState extends State { + List locations = []; + @override + Widget build(BuildContext context) { + locations = context.controller().locations; + + return Scaffold( + appBar: AppBar(title: Text('Manage Locations')), + body: Padding( + padding: EdgeInsetsGeometry.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.05, + ), + child: ListView.builder( + itemBuilder: listViewBuilder, + itemCount: context.controller().locations.length, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: onAddLocationButtonPressed, + child: Icon(Icons.add), + ), + ); + } + + Widget listViewBuilder(BuildContext context, int index) { + final location = locations.elementAt(index); + final String subtitle = location.address.isEmpty + ? location.coordinates.toString() + : location.address; + + return ListTile( + title: Text(location.name), + subtitle: Text(subtitle), + onTap: () => onEditLocationButtonPressed(location), + ); + } + + void onAddLocationButtonPressed() async { + final result = await showDialog( + context: context, + builder: (context) => CreateLocationDialog(), + barrierDismissible: false, + ); + if (mounted && result != null) { + context.controller().addLocation(result); + } + } + + void onEditLocationButtonPressed(Location location) async { + final result = await showDialog( + context: context, + builder: (context) => CreateLocationDialog(initialLocation: location), + barrierDismissible: false, + ); + if (mounted && result != null) { + context.controller().updateLocation(location, result); + } + } +} diff --git a/lib/pages/task_overview_page.dart b/lib/pages/task_overview_page.dart index 6ef7ddd..43aa664 100644 --- a/lib/pages/task_overview_page.dart +++ b/lib/pages/task_overview_page.dart @@ -6,6 +6,7 @@ import '../model/task.dart'; import '../service/controllers/task_controller.dart'; import '../service/tools.dart'; import '../widgets/task_dismissible.dart'; +import 'locations_overview_page.dart'; import 'task_edit_page.dart'; class TaskOverviewPage extends StatefulWidget { @@ -22,7 +23,20 @@ class _TaskOverviewPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Hallo Yannick')), + appBar: AppBar( + title: Text('Hallo Yannick'), + actions: [ + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + onTap: onLocationsButtonTapped, + child: Text('Locations'), + ), + ], + icon: Icon(Icons.more_vert), + ), + ], + ), body: Padding( padding: EdgeInsetsGeometry.symmetric( horizontal: MediaQuery.of(context).size.width * 0.05, @@ -84,10 +98,13 @@ class _TaskOverviewPageState extends State { await Navigator.of(context).pushNamed(TaskEditPage.routeName) as CreateTaskRequest?; - if (result != null && context.mounted) { + if (result != null && mounted) { context.controller().saveTask( result.toTask(id: generateId()), ); } } + + void onLocationsButtonTapped() => + Navigator.of(context).pushNamed(LocationsOverviewPage.routeName); } diff --git a/lib/widgets/dialogs/create_location_dialog.dart b/lib/widgets/dialogs/create_location_dialog.dart new file mode 100644 index 0000000..0f47ce9 --- /dev/null +++ b/lib/widgets/dialogs/create_location_dialog.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +import '../../app_theme.dart'; +import '../../model/latlng.dart'; +import '../../model/location.dart'; +import '../../service/validators.dart'; + +class CreateLocationDialog extends StatefulWidget { + const CreateLocationDialog({super.key, this.initialLocation}); + final Location? initialLocation; + + @override + State createState() => _CreateLocationDialogState(); +} + +class _CreateLocationDialogState extends State { + final nameController = TextEditingController(); + final addressController = TextEditingController(); + final coordinatesController = TextEditingController(); + final formKey = GlobalKey(debugLabel: 'Create Location Form'); + + @override + void initState() { + if (widget.initialLocation != null) { + nameController.text = widget.initialLocation!.name; + addressController.text = widget.initialLocation!.address; + coordinatesController.text = widget.initialLocation!.coordinates + .toString(); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + actions: [ + TextButton(onPressed: onCancelPressed, child: Text('Cancel')), + TextButton(onPressed: onSavePressed, child: Text('Save')), + ], + title: Text('Create Location'), + content: Form( + key: formKey, + child: Column( + spacing: AppTheme.formColumnSpacing, + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + autofocus: true, + textInputAction: TextInputAction.next, + controller: nameController, + keyboardType: TextInputType.text, + decoration: InputDecoration(labelText: 'Name'), + validator: notEmptyValidator, + ), + TextFormField( + textInputAction: TextInputAction.next, + controller: addressController, + keyboardType: TextInputType.streetAddress, + decoration: InputDecoration(labelText: 'Address (optional)'), + ), + TextFormField( + textInputAction: TextInputAction.done, + controller: coordinatesController, + onFieldSubmitted: (_) => onSavePressed(), + keyboardType: TextInputType.numberWithOptions(), + decoration: InputDecoration( + labelText: 'Coordinates', + hint: Text('25.5892, 50.5051662'), + ), + validator: (value) { + return notEmptyValidator(value) ?? coordinatesValidator(value); + }, + ), + ], + ), + ), + ); + } + + void onCancelPressed() => Navigator.of(context).pop(); + + void onSavePressed() { + if (formKey.currentState!.validate()) { + Navigator.of(context).pop( + Location( + name: nameController.text, + coordinates: LatLng.fromString(coordinatesController.text), + address: addressController.text, + ), + ); + } + } +}