12 Commits
11 changed files with 241 additions and 52 deletions
+12 -5
View File
@@ -9,12 +9,12 @@ abstract class Alarm {
factory Alarm.fromJson(Map<String, dynamic> json) { factory Alarm.fromJson(Map<String, dynamic> json) {
if (json.containsKey('alarmType')) { if (json.containsKey('alarmType')) {
switch (json['alarmType'] as String) { switch (AlarmType.fromJson(json['alarmType'])) {
case 'timeFixed': case AlarmType.timeFixed:
return FixedTimeAlarm.fromJson(json); return FixedTimeAlarm.fromJson(json);
case 'timeRelative': case AlarmType.timeRelative:
return RelativeTimeAlarm.fromJson(json); return RelativeTimeAlarm.fromJson(json);
case 'location': case AlarmType.location:
return LocationAlarm.fromJson(json); return LocationAlarm.fromJson(json);
} }
} }
@@ -33,4 +33,11 @@ abstract class Alarm {
int get hashCode => id.hashCode; int get hashCode => id.hashCode;
} }
enum AlarmType { timeFixed, timeRelative, location } enum AlarmType {
timeFixed,
timeRelative,
location;
String toJson() => name;
static AlarmType fromJson(String json) => values.byName(json);
}
+1 -1
View File
@@ -24,7 +24,7 @@ class FixedTimeAlarm implements Alarm {
return { return {
'id': id, 'id': id,
'triggerAt': triggerAt.toIso8601String(), 'triggerAt': triggerAt.toIso8601String(),
'alarmType': alarmType, 'alarmType': alarmType.toJson(),
}; };
} }
} }
+1 -1
View File
@@ -32,7 +32,7 @@ class LocationAlarm implements Alarm {
'id': id, 'id': id,
'location': location.toJson(), 'location': location.toJson(),
'radiusMeters': radiusMeters, 'radiusMeters': radiusMeters,
'alarmType': alarmType, 'alarmType': alarmType.toJson(),
}; };
} }
} }
+4 -4
View File
@@ -4,7 +4,7 @@ class RelativeTimeAlarm implements Alarm {
@override @override
final String id; final String id;
final DateTime triggerAt; final Duration triggerAt;
@override @override
final AlarmType alarmType; final AlarmType alarmType;
@@ -15,7 +15,7 @@ class RelativeTimeAlarm implements Alarm {
factory RelativeTimeAlarm.fromJson(Map<String, dynamic> json) { factory RelativeTimeAlarm.fromJson(Map<String, dynamic> json) {
return RelativeTimeAlarm( return RelativeTimeAlarm(
id: json['id'] as String, id: json['id'] as String,
triggerAt: DateTime.parse(json['triggerAt'] as String), triggerAt: Duration(minutes: json['triggerAt'] as int),
); );
} }
@@ -23,8 +23,8 @@ class RelativeTimeAlarm implements Alarm {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
'triggerAt': triggerAt.toIso8601String(), 'triggerAt': triggerAt.inMinutes,
'alarmType': alarmType, 'alarmType': alarmType.toJson(),
}; };
} }
} }
+4 -2
View File
@@ -1,7 +1,9 @@
import '../alarm/alarm.dart' show AlarmType; import '../alarm/alarm.dart' show AlarmType, Alarm;
class CreateAlarmRequest { abstract class CreateAlarmRequest {
const CreateAlarmRequest({required this.alarmType}); const CreateAlarmRequest({required this.alarmType});
final AlarmType alarmType; final AlarmType alarmType;
Alarm toAlarm(String id);
} }
@@ -12,6 +12,7 @@ class CreateFixedTimeAlarmRequest extends CreateAlarmRequest {
: triggerAt = alarm.triggerAt, : triggerAt = alarm.triggerAt,
super(alarmType: AlarmType.timeFixed); super(alarmType: AlarmType.timeFixed);
FixedTimeAlarm toFixedTimeAlarm(String id) => @override
FixedTimeAlarm toAlarm(String id) =>
FixedTimeAlarm(id: id, triggerAt: triggerAt!); FixedTimeAlarm(id: id, triggerAt: triggerAt!);
} }
@@ -16,6 +16,7 @@ class CreateLocationAlarmRequest extends CreateAlarmRequest {
radiusMeters = alarm.radiusMeters, radiusMeters = alarm.radiusMeters,
super(alarmType: AlarmType.location); super(alarmType: AlarmType.location);
LocationAlarm toLocationAlarm(String id) => @override
LocationAlarm toAlarm(String id) =>
LocationAlarm(id: id, location: location!, radiusMeters: radiusMeters!); LocationAlarm(id: id, location: location!, radiusMeters: radiusMeters!);
} }
@@ -3,7 +3,7 @@ import '../alarm/relative_time_alarm.dart';
import 'create_alarm_request.dart'; import 'create_alarm_request.dart';
class CreateRelativeTimeAlarmRequest extends CreateAlarmRequest { class CreateRelativeTimeAlarmRequest extends CreateAlarmRequest {
final DateTime? triggerAt; final Duration? triggerAt;
const CreateRelativeTimeAlarmRequest({this.triggerAt}) const CreateRelativeTimeAlarmRequest({this.triggerAt})
: super(alarmType: AlarmType.timeRelative); : super(alarmType: AlarmType.timeRelative);
@@ -12,6 +12,7 @@ class CreateRelativeTimeAlarmRequest extends CreateAlarmRequest {
: triggerAt = alarm.triggerAt, : triggerAt = alarm.triggerAt,
super(alarmType: AlarmType.timeRelative); super(alarmType: AlarmType.timeRelative);
RelativeTimeAlarm toRelativeTimeAlarm(String id) => @override
RelativeTimeAlarm toAlarm(String id) =>
RelativeTimeAlarm(id: id, triggerAt: triggerAt!); RelativeTimeAlarm(id: id, triggerAt: triggerAt!);
} }
+51 -10
View File
@@ -2,9 +2,13 @@ import 'package:flutter/material.dart';
import '../app_theme.dart'; import '../app_theme.dart';
import '../model/alarm/alarm.dart'; import '../model/alarm/alarm.dart';
import '../model/alarm/fixed_time_alarm.dart';
import '../model/alarm/location_alarm.dart';
import '../model/alarm/relative_time_alarm.dart';
import '../model/requests/create_alarm_request.dart'; import '../model/requests/create_alarm_request.dart';
import '../model/requests/create_fixed_time_alarm_request.dart'; import '../model/requests/create_fixed_time_alarm_request.dart';
import '../model/requests/create_location_alarm_request.dart'; import '../model/requests/create_location_alarm_request.dart';
import '../model/requests/create_relative_time_alarm_request.dart';
import '../model/requests/create_task_request.dart'; import '../model/requests/create_task_request.dart';
import '../model/extensions/controller_context.dart'; import '../model/extensions/controller_context.dart';
import '../model/task.dart'; import '../model/task.dart';
@@ -37,7 +41,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 = []; final List<CreateAlarmRequest> alarmRequests = [];
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@@ -56,6 +60,18 @@ class _TaskEditPageState extends State<TaskEditPage> {
isDueTimeEnabled = true; isDueTimeEnabled = true;
} }
isInitialized = true; isInitialized = true;
alarmRequests.addAll(
task!.alarms.map((alarm) {
if (alarm is FixedTimeAlarm) {
return CreateFixedTimeAlarmRequest.fromAlarm(alarm);
} else if (alarm is RelativeTimeAlarm) {
return CreateRelativeTimeAlarmRequest.fromAlarm(alarm);
} else if (alarm is LocationAlarm) {
return CreateLocationAlarmRequest.fromAlarm(alarm);
}
throw TypeError();
}),
);
pageTitle = task!.title; pageTitle = task!.title;
} }
} }
@@ -151,15 +167,28 @@ class _TaskEditPageState extends State<TaskEditPage> {
label: Text('Location'), label: Text('Location'),
icon: Icon(Icons.location_on), icon: Icon(Icons.location_on),
), ),
for (final (index, alarm) in alarms.indexed) for (final (index, alarmRequest) in alarmRequests.indexed)
AlarmForm( Row(
request: alarm, children: [
Expanded(
child: AlarmForm(
request: alarmRequest,
onChanged: (updated) { onChanged: (updated) {
setState(() { setState(() {
alarms[index] = updated; alarmRequests[index] = updated;
}); });
}, },
), ),
),
IconButton(
onPressed: () => setState(() {
alarmRequests.removeAt(index);
}),
icon: Icon(Icons.close),
),
],
),
SizedBox(height: 120),
], ],
), ),
], ],
@@ -175,6 +204,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
} }
void onSavePressed() { void onSavePressed() {
if (formKey.currentState!.validate()) {
Navigator.of(context).pop( Navigator.of(context).pop(
CreateTaskRequest( CreateTaskRequest(
title: titleController.text, title: titleController.text,
@@ -187,10 +217,22 @@ class _TaskEditPageState extends State<TaskEditPage> {
category: categoryController.text, category: categoryController.text,
subtasks: [], subtasks: [],
url: urlController.text, url: urlController.text,
alarms: [], alarms: alarmRequests.map<Alarm>((e) {
if (e is CreateFixedTimeAlarmRequest) {
return e.toAlarm(generateId());
}
if (e is CreateRelativeTimeAlarmRequest) {
return e.toAlarm(generateId());
}
if (e is LocationAlarm) {
return e.toAlarm(generateId());
}
throw TypeError();
}).toList(),
), ),
); );
} }
}
void onPopInvoked(bool didPop, Object? result) { void onPopInvoked(bool didPop, Object? result) {
if (!didPop) { if (!didPop) {
@@ -223,12 +265,11 @@ class _TaskEditPageState extends State<TaskEditPage> {
void onAddAlarmPressed(AlarmType alarmType) { void onAddAlarmPressed(AlarmType alarmType) {
switch (alarmType) { switch (alarmType) {
case AlarmType.location: case AlarmType.location:
alarms.add(CreateLocationAlarmRequest()); alarmRequests.add(CreateLocationAlarmRequest());
case AlarmType.timeFixed: case AlarmType.timeFixed:
alarms.add(CreateFixedTimeAlarmRequest()); alarmRequests.add(CreateFixedTimeAlarmRequest());
case AlarmType.timeRelative: case AlarmType.timeRelative:
alarms.add(CreateFixedTimeAlarmRequest()); alarmRequests.add(CreateRelativeTimeAlarmRequest());
} }
setState(() {}); setState(() {});
} }
@@ -26,17 +26,26 @@ class _FixedTimeAlarmFormState extends State<FixedTimeAlarmForm> {
bool isDueTimeEnabled = false; bool isDueTimeEnabled = false;
@override @override
void initState() { void didChangeDependencies() {
if (widget.request.triggerAt != null) {
dueDateController.text = getIsoDateString(widget.request.triggerAt!);
dueTimeController.text = TimeOfDay.fromDateTime(
widget.request.triggerAt!,
).format(context);
isDueTimeEnabled = true;
}
dueDateController.addListener(onDueDateChanged); dueDateController.addListener(onDueDateChanged);
dueTimeController.addListener(onDueTimeChanged); dueTimeController.addListener(onDueTimeChanged);
super.initState(); super.didChangeDependencies();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Flex( return Flex(
direction: Axis.horizontal, direction: Axis.horizontal,
spacing: AppTheme.formColumnSpacing,
children: [ children: [
Flexible( Flexible(
flex: 3, flex: 3,
@@ -61,14 +70,13 @@ class _FixedTimeAlarmFormState extends State<FixedTimeAlarmForm> {
icon: Icon(Icons.calendar_month), icon: Icon(Icons.calendar_month),
), ),
), ),
validator: dateTimeValidator, validator: (value) {
return notEmptyValidator(value) ?? dateTimeValidator(value);
},
keyboardType: TextInputType.datetime, keyboardType: TextInputType.datetime,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
), ),
), ),
Padding(
padding: EdgeInsetsGeometry.only(left: AppTheme.formColumnSpacing),
),
Flexible( Flexible(
flex: 2, flex: 2,
child: TextFormField( child: TextFormField(
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../app_theme.dart';
import '../../model/requests/create_alarm_request.dart'; import '../../model/requests/create_alarm_request.dart';
import '../../model/requests/create_relative_time_alarm_request.dart'; import '../../model/requests/create_relative_time_alarm_request.dart';
import '../../service/validators.dart';
class RelativeTimeAlarmForm extends StatelessWidget { class RelativeTimeAlarmForm extends StatefulWidget {
const RelativeTimeAlarmForm({ const RelativeTimeAlarmForm({
super.key, super.key,
required this.request, required this.request,
@@ -12,8 +14,134 @@ class RelativeTimeAlarmForm extends StatelessWidget {
final CreateRelativeTimeAlarmRequest request; final CreateRelativeTimeAlarmRequest request;
final ValueChanged<CreateAlarmRequest> onChanged; final ValueChanged<CreateAlarmRequest> onChanged;
@override
State<RelativeTimeAlarmForm> createState() => _RelativeTimeAlarmFormState();
}
class _RelativeTimeAlarmFormState extends State<RelativeTimeAlarmForm> {
final timeController = TextEditingController();
static const List<DropdownMenuEntry<TimeUnit>> timeUnitOptions = [
DropdownMenuEntry<TimeUnit>(value: TimeUnit.minute, label: 'Minutes'),
DropdownMenuEntry<TimeUnit>(value: TimeUnit.hour, label: 'Hours'),
DropdownMenuEntry<TimeUnit>(value: TimeUnit.day, label: 'Days'),
];
static const List<DropdownMenuEntry<TimeRelation>> timeRelationOptions = [
DropdownMenuEntry<TimeRelation>(
value: TimeRelation.before,
label: 'Before',
),
DropdownMenuEntry<TimeRelation>(value: TimeRelation.after, label: 'After'),
];
late TimeUnit selectedTimeUnit;
late TimeRelation selectedTimeRelation;
@override
void initState() {
super.initState();
if (widget.request.triggerAt == null) {
selectedTimeUnit = TimeUnit.minute;
selectedTimeRelation = TimeRelation.before;
return;
}
final duration = widget.request.triggerAt!;
selectedTimeRelation = duration.isNegative
? TimeRelation.before
: TimeRelation.after;
final absDuration = duration.abs();
if (absDuration.inMinutes <= 99 && absDuration.inMinutes % 60 != 0) {
selectedTimeUnit = TimeUnit.minute;
timeController.text = absDuration.inMinutes.toString();
} else if (absDuration.inHours <= 99 && absDuration.inHours % 24 != 0) {
selectedTimeUnit = TimeUnit.hour;
timeController.text = absDuration.inHours.toString();
} else {
selectedTimeUnit = TimeUnit.day;
timeController.text = absDuration.inDays.toString();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Placeholder(); return Flex(
direction: Axis.horizontal,
spacing: AppTheme.formColumnSpacing,
children: [
Flexible(
flex: 7,
child: TextFormField(
controller: timeController,
maxLength: 2,
onChanged: onTimeChanged,
validator: (value) =>
notEmptyValidator(value) ?? wholeNumberValidator(value),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
decoration: InputDecoration(label: Text('Time'), counterText: ''),
),
),
Flexible(
flex: 10,
child: DropdownMenuFormField<TimeUnit>(
dropdownMenuEntries: timeUnitOptions,
initialSelection: selectedTimeUnit,
showTrailingIcon: false,
onSelected: (value) {
selectedTimeUnit = value!;
onTimeChanged(timeController.text);
},
),
),
Flexible(
flex: 10,
child: DropdownMenuFormField<TimeRelation>(
dropdownMenuEntries: timeRelationOptions,
initialSelection: selectedTimeRelation,
showTrailingIcon: false,
onSelected: (value) {
selectedTimeRelation = value!;
onTimeChanged(timeController.text);
},
),
),
],
);
}
void onTimeChanged(String value) {
int? time = int.tryParse(value);
if (wholeNumberValidator(value) != null || time == null) return;
Duration duration;
int signedTime = selectedTimeRelation.apply(time);
switch (selectedTimeUnit) {
case TimeUnit.minute:
duration = Duration(minutes: signedTime);
case TimeUnit.hour:
duration = Duration(hours: signedTime);
case TimeUnit.day:
duration = Duration(days: signedTime);
}
widget.onChanged(CreateRelativeTimeAlarmRequest(triggerAt: duration));
} }
} }
enum TimeRelation {
before,
after;
int apply(int value) => switch (this) {
TimeRelation.before => -value,
TimeRelation.after => value,
};
}
enum TimeUnit { minute, hour, day }