10 Commits
Author SHA1 Message Date
marco 3ceb0896a4 created alram form widget placeholders 2026-07-17 16:58:05 +02:00
marco c7ff39e55c refactored alarm models 2026-07-17 16:57:55 +02:00
marco a33c4bbb12 changed theme 2026-07-16 16:45:18 +02:00
marco 5fc290dbcb added alarm type 2026-07-16 16:45:01 +02:00
marco 463a2c8fda added alarmtype enum 2026-07-16 16:44:45 +02:00
marco dcae40497d removed required fields 2026-07-16 16:15:33 +02:00
marco a440c29043 made fields nullable 2026-07-16 16:13:25 +02:00
marco e1eafdca59 added validator for whole numbers 2026-07-16 16:10:33 +02:00
marco 4af3683b73 changed dropdownmenu and button theme 2026-07-16 16:10:22 +02:00
marco 505aff4232 added create models for alarms 2026-07-16 16:10:02 +02:00
16 changed files with 354 additions and 55 deletions
+20
View File
@@ -34,6 +34,26 @@ class AppTheme {
side: BorderSide(color: colorScheme.secondaryContainer, width: 2), side: BorderSide(color: colorScheme.secondaryContainer, width: 2),
), ),
), ),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
minimumSize: WidgetStatePropertyAll(Size(0, 56)),
shape: WidgetStateOutlinedBorder.resolveWith(
(states) =>
RoundedRectangleBorder(borderRadius: universalBorderRadius),
),
),
),
dropdownMenuTheme: DropdownMenuThemeData(
inputDecorationTheme: InputDecorationThemeData(
border: OutlineInputBorder(borderRadius: universalBorderRadius),
),
menuStyle: MenuStyle(
shape: WidgetStateOutlinedBorder.resolveWith(
(states) =>
RoundedRectangleBorder(borderRadius: universalBorderRadius),
),
),
),
); );
} }
} }
+14 -7
View File
@@ -1,18 +1,23 @@
import 'fixed_time_alarm.dart';
import 'location_alarm.dart'; import 'location_alarm.dart';
import 'time_alarm.dart'; import 'relative_time_alarm.dart';
abstract class Alarm { abstract class Alarm {
String get id; String get id;
String get taskId; String get taskId;
AlarmType get alarmType;
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
factory Alarm.fromJson(Map<String, dynamic> json) { factory Alarm.fromJson(Map<String, dynamic> json) {
if (json.containsKey('triggerAt')) { if (json.containsKey('alarmType')) {
return TimeAlarm.fromJson(json); switch (json['alarmType'] as String) {
} case 'timeFixed':
return FixedTimeAlarm.fromJson(json);
if (json.containsKey('location')) { case 'timeRelative':
return LocationAlarm.fromJson(json); return RelativeTimeAlarm.fromJson(json);
case 'location':
return LocationAlarm.fromJson(json);
}
} }
throw ArgumentError('Unknown alarm type'); throw ArgumentError('Unknown alarm type');
@@ -28,3 +33,5 @@ abstract class Alarm {
@override @override
int get hashCode => taskId.hashCode; int get hashCode => taskId.hashCode;
} }
enum AlarmType { timeFixed, timeRelative, location }
@@ -0,0 +1,7 @@
import '../alarm.dart' show AlarmType;
class CreateAlarmRequest {
const CreateAlarmRequest({required this.alarmType});
final AlarmType alarmType;
}
@@ -0,0 +1,9 @@
import '../alarm.dart';
import 'create_alarm_request.dart';
class CreateFixedTimeAlarmRequest extends CreateAlarmRequest {
final DateTime? triggerAt;
CreateFixedTimeAlarmRequest({this.triggerAt})
: super(alarmType: AlarmType.timeFixed);
}
@@ -0,0 +1,12 @@
import '../alarm.dart' show AlarmType;
import '../location.dart';
import 'create_alarm_request.dart';
class CreateLocationAlarmRequest extends CreateAlarmRequest {
final Location? location;
final int? radiusMeters;
const CreateLocationAlarmRequest({this.location, this.radiusMeters})
: super(alarmType: AlarmType.location);
}
@@ -0,0 +1,9 @@
import '../alarm.dart' show AlarmType;
import 'create_alarm_request.dart';
class CreateRelativeTimeAlarmRequest extends CreateAlarmRequest {
final DateTime? triggerAt;
CreateRelativeTimeAlarmRequest({this.triggerAt})
: super(alarmType: AlarmType.timeRelative);
}
@@ -1,6 +1,6 @@
import 'alarm.dart'; import 'alarm.dart';
class TimeAlarm implements Alarm { class FixedTimeAlarm implements Alarm {
@override @override
final String id; final String id;
@@ -9,14 +9,17 @@ class TimeAlarm implements Alarm {
final DateTime triggerAt; final DateTime triggerAt;
const TimeAlarm({ @override
final AlarmType alarmType;
const FixedTimeAlarm({
required this.id, required this.id,
required this.taskId, required this.taskId,
required this.triggerAt, required this.triggerAt,
}); }) : alarmType = AlarmType.timeFixed;
factory TimeAlarm.fromJson(Map<String, dynamic> json) { factory FixedTimeAlarm.fromJson(Map<String, dynamic> json) {
return TimeAlarm( return FixedTimeAlarm(
id: json['id'] as String, id: json['id'] as String,
taskId: json['taskId'] as String, taskId: json['taskId'] as String,
triggerAt: DateTime.parse(json['triggerAt'] as String), triggerAt: DateTime.parse(json['triggerAt'] as String),
@@ -29,6 +32,7 @@ class TimeAlarm implements Alarm {
'id': id, 'id': id,
'taskId': taskId, 'taskId': taskId,
'triggerAt': triggerAt.toIso8601String(), 'triggerAt': triggerAt.toIso8601String(),
'alarmType': alarmType,
}; };
} }
} }
+5 -1
View File
@@ -12,12 +12,15 @@ class LocationAlarm implements Alarm {
final int radiusMeters; final int radiusMeters;
@override
final AlarmType alarmType;
const LocationAlarm({ const LocationAlarm({
required this.id, required this.id,
required this.taskId, required this.taskId,
required this.location, required this.location,
required this.radiusMeters, required this.radiusMeters,
}); }) : alarmType = AlarmType.location;
factory LocationAlarm.fromJson(Map<String, dynamic> json) { factory LocationAlarm.fromJson(Map<String, dynamic> json) {
return LocationAlarm( return LocationAlarm(
@@ -35,6 +38,7 @@ class LocationAlarm implements Alarm {
'taskId': taskId, 'taskId': taskId,
'location': location.toJson(), 'location': location.toJson(),
'radiusMeters': radiusMeters, 'radiusMeters': radiusMeters,
'alarmType': alarmType,
}; };
} }
} }
+38
View File
@@ -0,0 +1,38 @@
import 'alarm.dart';
class RelativeTimeAlarm implements Alarm {
@override
final String id;
@override
final String taskId;
final DateTime triggerAt;
@override
final AlarmType alarmType;
const RelativeTimeAlarm({
required this.id,
required this.taskId,
required this.triggerAt,
}) : alarmType = AlarmType.timeRelative;
factory RelativeTimeAlarm.fromJson(Map<String, dynamic> json) {
return RelativeTimeAlarm(
id: json['id'] as String,
taskId: json['taskId'] as String,
triggerAt: DateTime.parse(json['triggerAt'] as String),
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'taskId': taskId,
'triggerAt': triggerAt.toIso8601String(),
'alarmType': alarmType,
};
}
}
+97 -40
View File
@@ -1,11 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../app_theme.dart'; import '../app_theme.dart';
import '../model/alarm.dart';
import '../model/callback_models/create_alarm_request.dart';
import '../model/callback_models/create_fixed_time_alarm_request.dart';
import '../model/callback_models/create_location_alarm_request.dart';
import '../model/callback_models/create_task_request.dart'; import '../model/callback_models/create_task_request.dart';
import '../model/extensions/controller_context.dart'; import '../model/extensions/controller_context.dart';
import '../model/task.dart'; import '../model/task.dart';
import '../service/controllers/task_controller.dart'; import '../service/controllers/task_controller.dart';
import '../service/tools.dart'; import '../service/tools.dart';
import '../widgets/alarm_widgets/alarm_form_widget.dart';
import '../widgets/time_selector.dart'; import '../widgets/time_selector.dart';
class TaskEditPage extends StatefulWidget { class TaskEditPage extends StatefulWidget {
@@ -32,6 +37,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
final formKey = GlobalKey<FormState>(debugLabel: 'taskEditFormKey'); final formKey = GlobalKey<FormState>(debugLabel: 'taskEditFormKey');
bool didFormChange = false; bool didFormChange = false;
bool isDueTimeEnabled = false; bool isDueTimeEnabled = false;
final List<CreateAlarmRequest> alarms = [];
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@@ -80,46 +86,84 @@ class _TaskEditPageState extends State<TaskEditPage> {
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.05, horizontal: MediaQuery.of(context).size.width * 0.05,
), ),
child: Column( child: SingleChildScrollView(
spacing: AppTheme.formColumnSpacing, child: Column(
children: [ spacing: AppTheme.formColumnSpacing,
SizedBox(height: 6), children: [
TextFormField( SizedBox(height: 6),
autofocus: true, TextFormField(
controller: titleController, autofocus: true,
decoration: InputDecoration(label: Text('Title')), controller: titleController,
keyboardType: TextInputType.text, decoration: InputDecoration(label: Text('Title')),
textInputAction: TextInputAction.next, keyboardType: TextInputType.text,
), textInputAction: TextInputAction.next,
TextFormField( ),
controller: descriptionController, TextFormField(
decoration: InputDecoration(label: Text('Description')), controller: descriptionController,
keyboardType: TextInputType.multiline, decoration: InputDecoration(label: Text('Description')),
textInputAction: TextInputAction.newline, keyboardType: TextInputType.multiline,
minLines: 3, textInputAction: TextInputAction.newline,
maxLines: 10, minLines: 3,
), maxLines: 10,
TimeSelector( ),
initialDueDateTime: task?.due, TimeSelector(
nextFocusNode: categoryFocusNode, initialDueDateTime: task?.due,
dueDateController: dueDateController, nextFocusNode: categoryFocusNode,
dueTimeController: dueTimeController, dueDateController: dueDateController,
formKey: formKey, dueTimeController: dueTimeController,
), formKey: formKey,
TextFormField( ),
focusNode: categoryFocusNode, TextFormField(
controller: categoryController, focusNode: categoryFocusNode,
decoration: InputDecoration(label: Text('Category')), controller: categoryController,
keyboardType: TextInputType.text, decoration: InputDecoration(label: Text('Category')),
textInputAction: TextInputAction.next, keyboardType: TextInputType.text,
), textInputAction: TextInputAction.next,
TextFormField( ),
controller: urlController, TextFormField(
decoration: InputDecoration(label: Text('Url')), controller: urlController,
keyboardType: TextInputType.text, decoration: InputDecoration(label: Text('Url')),
textInputAction: TextInputAction.next, keyboardType: TextInputType.text,
), textInputAction: TextInputAction.next,
], ),
Text(
'Alarms',
style: Theme.of(context).textTheme.headlineLarge,
),
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
spacing: AppTheme.formColumnSpacing,
children: [
ElevatedButton.icon(
onPressed: () => onAddAlarmPressed(AlarmType.timeFixed),
label: Text('Fixed Time'),
icon: Icon(Icons.alarm_add),
),
ElevatedButton.icon(
onPressed: () =>
onAddAlarmPressed(AlarmType.timeRelative),
label: Text('Relative Time'),
icon: Icon(Icons.timer_outlined),
),
ElevatedButton.icon(
onPressed: () => onAddAlarmPressed(AlarmType.location),
label: Text('Location'),
icon: Icon(Icons.location_on),
),
for (final (index, alarm) in alarms.indexed)
AlarmForm(
request: alarm,
onChanged: (updated) {
setState(() {
alarms[index] = updated;
});
},
),
],
),
],
),
), ),
), ),
), ),
@@ -174,4 +218,17 @@ class _TaskEditPageState extends State<TaskEditPage> {
}); });
} }
} }
void onAddAlarmPressed(AlarmType alarmType) {
switch (alarmType) {
case AlarmType.location:
alarms.add(CreateLocationAlarmRequest());
case AlarmType.timeFixed:
alarms.add(CreateFixedTimeAlarmRequest());
case AlarmType.timeRelative:
alarms.add(CreateFixedTimeAlarmRequest());
}
setState(() {});
}
} }
+9
View File
@@ -7,6 +7,15 @@ String? timeValidator(String? value) {
return 'Not a valid time format'; return 'Not a valid time format';
} }
String? wholeNumberValidator(String? value) {
if (value == null || value.isEmpty) return null;
if (RegExp(r'^[0-9]*$').hasMatch(value)) {
return null;
}
return 'Not a valid number';
}
String? dateTimeValidator(String? value) { String? dateTimeValidator(String? value) {
if (value == null || value.isEmpty) return null; if (value == null || value.isEmpty) return null;
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import '../../model/alarm.dart' show AlarmType;
import '../../model/callback_models/create_alarm_request.dart';
import '../../model/callback_models/create_fixed_time_alarm_request.dart';
import '../../model/callback_models/create_location_alarm_request.dart';
import '../../model/callback_models/create_relative_time_alarm_request.dart';
import 'fixed_time_alarm_form.dart';
import 'location_alarm_form.dart';
import 'relative_time_alarm_form.dart';
class AlarmForm extends StatelessWidget {
const AlarmForm({super.key, required this.request, required this.onChanged});
final CreateAlarmRequest request;
final ValueChanged<CreateAlarmRequest> onChanged;
@override
Widget build(BuildContext context) {
switch (request.alarmType) {
case AlarmType.timeFixed:
return FixedTimeAlarmForm(
request: request as CreateFixedTimeAlarmRequest,
onChanged: onChanged,
);
case AlarmType.timeRelative:
return RelativeTimeAlarmForm(
request: request as CreateRelativeTimeAlarmRequest,
onChanged: onChanged,
);
case AlarmType.location:
return LocationAlarmForm(
request: request as CreateLocationAlarmRequest,
onChanged: onChanged,
);
}
}
}
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import '../../model/callback_models/create_alarm_request.dart';
import '../../model/callback_models/create_fixed_time_alarm_request.dart';
class FixedTimeAlarmForm extends StatelessWidget {
const FixedTimeAlarmForm({
super.key,
required this.request,
required this.onChanged,
});
final CreateFixedTimeAlarmRequest request;
final ValueChanged<CreateAlarmRequest> onChanged;
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import '../../model/callback_models/create_alarm_request.dart';
import '../../model/callback_models/create_location_alarm_request.dart';
class LocationAlarmForm extends StatelessWidget {
const LocationAlarmForm({
super.key,
required this.request,
required this.onChanged,
});
final CreateLocationAlarmRequest request;
final ValueChanged<CreateAlarmRequest> onChanged;
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import '../../model/callback_models/create_alarm_request.dart';
import '../../model/callback_models/create_relative_time_alarm_request.dart';
class RelativeTimeAlarmForm extends StatelessWidget {
const RelativeTimeAlarmForm({
super.key,
required this.request,
required this.onChanged,
});
final CreateRelativeTimeAlarmRequest request;
final ValueChanged<CreateAlarmRequest> onChanged;
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
+28 -2
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../app_theme.dart';
import '../service/tools.dart' show getIsoDateString; import '../service/tools.dart' show getIsoDateString;
import '../service/validators.dart'; import '../service/validators.dart';
@@ -11,6 +12,7 @@ class TimeSelector extends StatefulWidget {
required this.dueDateController, required this.dueDateController,
required this.dueTimeController, required this.dueTimeController,
this.formKey, this.formKey,
this.onChanged,
}); });
final DateTime? initialDueDateTime; final DateTime? initialDueDateTime;
@@ -18,6 +20,7 @@ class TimeSelector extends StatefulWidget {
final FocusNode? nextFocusNode; final FocusNode? nextFocusNode;
final TextEditingController dueDateController; final TextEditingController dueDateController;
final TextEditingController dueTimeController; final TextEditingController dueTimeController;
final void Function(DateTime dateTime)? onChanged;
@override @override
State<TimeSelector> createState() => _TimeSelectorState(); State<TimeSelector> createState() => _TimeSelectorState();
@@ -47,7 +50,18 @@ class _TimeSelectorState extends State<TimeSelector> {
child: TextFormField( child: TextFormField(
focusNode: dueDateFocusNode, focusNode: dueDateFocusNode,
controller: widget.dueDateController, controller: widget.dueDateController,
onChanged: maybeEnableDueTime, onChanged: (value) {
maybeEnableDueTime(value);
if (dateTimeValidator(value) == null &&
widget.onChanged != null) {
final dateTime = DateTime.tryParse(
'$value ${widget.dueTimeController.text}'.trim(),
);
if (dateTime != null) {
widget.onChanged!(dateTime);
}
}
},
onFieldSubmitted: (_) { onFieldSubmitted: (_) {
isDueTimeEnabled isDueTimeEnabled
? dueDateFocusNode.nextFocus() ? dueDateFocusNode.nextFocus()
@@ -74,7 +88,9 @@ class _TimeSelectorState extends State<TimeSelector> {
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
), ),
), ),
Padding(padding: EdgeInsetsGeometry.only(left: 10)), Padding(
padding: EdgeInsetsGeometry.only(left: AppTheme.formColumnSpacing),
),
Flexible( Flexible(
flex: 2, flex: 2,
child: TextFormField( child: TextFormField(
@@ -82,6 +98,16 @@ class _TimeSelectorState extends State<TimeSelector> {
onFieldSubmitted: (_) => dueTimeFocusNode.nextFocus(), onFieldSubmitted: (_) => dueTimeFocusNode.nextFocus(),
controller: widget.dueTimeController, controller: widget.dueTimeController,
enabled: isDueTimeEnabled, enabled: isDueTimeEnabled,
onChanged: (value) {
if (timeValidator(value) == null && widget.onChanged != null) {
final dateTime = DateTime.tryParse(
'${widget.dueDateController.text} $value'.trim(),
);
if (dateTime != null) {
widget.onChanged!(dateTime);
}
}
},
decoration: InputDecoration( decoration: InputDecoration(
label: Text('Due Time'), label: Text('Due Time'),
suffixIcon: IconButton( suffixIcon: IconButton(