Compare commits
10
Commits
21ef9c4ab5
...
3ceb0896a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ceb0896a4 | ||
|
|
c7ff39e55c | ||
|
|
a33c4bbb12 | ||
|
|
5fc290dbcb | ||
|
|
463a2c8fda | ||
|
|
dcae40497d | ||
|
|
a440c29043 | ||
|
|
e1eafdca59 | ||
|
|
4af3683b73 | ||
|
|
505aff4232 |
@@ -34,6 +34,26 @@ class AppTheme {
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-6
@@ -1,19 +1,24 @@
|
||||
import 'fixed_time_alarm.dart';
|
||||
import 'location_alarm.dart';
|
||||
import 'time_alarm.dart';
|
||||
import 'relative_time_alarm.dart';
|
||||
|
||||
abstract class Alarm {
|
||||
String get id;
|
||||
String get taskId;
|
||||
AlarmType get alarmType;
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
factory Alarm.fromJson(Map<String, dynamic> json) {
|
||||
if (json.containsKey('triggerAt')) {
|
||||
return TimeAlarm.fromJson(json);
|
||||
}
|
||||
|
||||
if (json.containsKey('location')) {
|
||||
if (json.containsKey('alarmType')) {
|
||||
switch (json['alarmType'] as String) {
|
||||
case 'timeFixed':
|
||||
return FixedTimeAlarm.fromJson(json);
|
||||
case 'timeRelative':
|
||||
return RelativeTimeAlarm.fromJson(json);
|
||||
case 'location':
|
||||
return LocationAlarm.fromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
throw ArgumentError('Unknown alarm type');
|
||||
}
|
||||
@@ -28,3 +33,5 @@ abstract class Alarm {
|
||||
@override
|
||||
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';
|
||||
|
||||
class TimeAlarm implements Alarm {
|
||||
class FixedTimeAlarm implements Alarm {
|
||||
@override
|
||||
final String id;
|
||||
|
||||
@@ -9,14 +9,17 @@ class TimeAlarm implements Alarm {
|
||||
|
||||
final DateTime triggerAt;
|
||||
|
||||
const TimeAlarm({
|
||||
@override
|
||||
final AlarmType alarmType;
|
||||
|
||||
const FixedTimeAlarm({
|
||||
required this.id,
|
||||
required this.taskId,
|
||||
required this.triggerAt,
|
||||
});
|
||||
}) : alarmType = AlarmType.timeFixed;
|
||||
|
||||
factory TimeAlarm.fromJson(Map<String, dynamic> json) {
|
||||
return TimeAlarm(
|
||||
factory FixedTimeAlarm.fromJson(Map<String, dynamic> json) {
|
||||
return FixedTimeAlarm(
|
||||
id: json['id'] as String,
|
||||
taskId: json['taskId'] as String,
|
||||
triggerAt: DateTime.parse(json['triggerAt'] as String),
|
||||
@@ -29,6 +32,7 @@ class TimeAlarm implements Alarm {
|
||||
'id': id,
|
||||
'taskId': taskId,
|
||||
'triggerAt': triggerAt.toIso8601String(),
|
||||
'alarmType': alarmType,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,15 @@ class LocationAlarm implements Alarm {
|
||||
|
||||
final int radiusMeters;
|
||||
|
||||
@override
|
||||
final AlarmType alarmType;
|
||||
|
||||
const LocationAlarm({
|
||||
required this.id,
|
||||
required this.taskId,
|
||||
required this.location,
|
||||
required this.radiusMeters,
|
||||
});
|
||||
}) : alarmType = AlarmType.location;
|
||||
|
||||
factory LocationAlarm.fromJson(Map<String, dynamic> json) {
|
||||
return LocationAlarm(
|
||||
@@ -35,6 +38,7 @@ class LocationAlarm implements Alarm {
|
||||
'taskId': taskId,
|
||||
'location': location.toJson(),
|
||||
'radiusMeters': radiusMeters,
|
||||
'alarmType': alarmType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'package:flutter/material.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/extensions/controller_context.dart';
|
||||
import '../model/task.dart';
|
||||
import '../service/controllers/task_controller.dart';
|
||||
import '../service/tools.dart';
|
||||
import '../widgets/alarm_widgets/alarm_form_widget.dart';
|
||||
import '../widgets/time_selector.dart';
|
||||
|
||||
class TaskEditPage extends StatefulWidget {
|
||||
@@ -32,6 +37,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
final formKey = GlobalKey<FormState>(debugLabel: 'taskEditFormKey');
|
||||
bool didFormChange = false;
|
||||
bool isDueTimeEnabled = false;
|
||||
final List<CreateAlarmRequest> alarms = [];
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -80,6 +86,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width * 0.05,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: AppTheme.formColumnSpacing,
|
||||
children: [
|
||||
@@ -119,8 +126,45 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
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;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
@@ -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(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,15 @@ String? timeValidator(String? value) {
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app_theme.dart';
|
||||
import '../service/tools.dart' show getIsoDateString;
|
||||
import '../service/validators.dart';
|
||||
|
||||
@@ -11,6 +12,7 @@ class TimeSelector extends StatefulWidget {
|
||||
required this.dueDateController,
|
||||
required this.dueTimeController,
|
||||
this.formKey,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
final DateTime? initialDueDateTime;
|
||||
@@ -18,6 +20,7 @@ class TimeSelector extends StatefulWidget {
|
||||
final FocusNode? nextFocusNode;
|
||||
final TextEditingController dueDateController;
|
||||
final TextEditingController dueTimeController;
|
||||
final void Function(DateTime dateTime)? onChanged;
|
||||
|
||||
@override
|
||||
State<TimeSelector> createState() => _TimeSelectorState();
|
||||
@@ -47,7 +50,18 @@ class _TimeSelectorState extends State<TimeSelector> {
|
||||
child: TextFormField(
|
||||
focusNode: dueDateFocusNode,
|
||||
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: (_) {
|
||||
isDueTimeEnabled
|
||||
? dueDateFocusNode.nextFocus()
|
||||
@@ -74,7 +88,9 @@ class _TimeSelectorState extends State<TimeSelector> {
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsetsGeometry.only(left: 10)),
|
||||
Padding(
|
||||
padding: EdgeInsetsGeometry.only(left: AppTheme.formColumnSpacing),
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: TextFormField(
|
||||
@@ -82,6 +98,16 @@ class _TimeSelectorState extends State<TimeSelector> {
|
||||
onFieldSubmitted: (_) => dueTimeFocusNode.nextFocus(),
|
||||
controller: widget.dueTimeController,
|
||||
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(
|
||||
label: Text('Due Time'),
|
||||
suffixIcon: IconButton(
|
||||
|
||||
Reference in New Issue
Block a user