3 Commits
Author SHA1 Message Date
marco 842d4b8390 Merge pull request 'Location overview screen and model changes' (#3) from development into main
Flutter APK Build / Build Flutter APK (push) Successful in 5m49s
Reviewed-on: #3
2026-06-19 14:03:45 +02:00
marco 81222de7fe Merge pull request 'Added app theming' (#2) from development into main
Flutter APK Build / Build Flutter APK (push) Successful in 5m44s
Reviewed-on: #2
2026-06-19 01:37:42 +02:00
marco 392ec22dcd Merge pull request 'Feature: dismissible tasks' (#1) from development into main
Flutter APK Build / Build Flutter APK (push) Successful in 5m43s
Reviewed-on: #1
2026-06-19 00:36:43 +02:00
28 changed files with 108 additions and 613 deletions
-20
View File
@@ -34,26 +34,6 @@ 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),
),
),
),
); );
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'app_theme.dart'; import 'app_theme.dart';
import 'repositories/local_repository.dart'; import 'model/repositories/local_repository.dart';
import 'pages/locations_overview_page.dart'; import 'pages/locations_overview_page.dart';
import 'pages/task_edit_page.dart'; import 'pages/task_edit_page.dart';
import 'pages/task_overview_page.dart'; import 'pages/task_overview_page.dart';
+30
View File
@@ -0,0 +1,30 @@
import 'location_alarm.dart';
import 'time_alarm.dart';
abstract class Alarm {
String get id;
String get taskId;
Map<String, dynamic> toJson();
factory Alarm.fromJson(Map<String, dynamic> json) {
if (json.containsKey('triggerAt')) {
return TimeAlarm.fromJson(json);
}
if (json.containsKey('location')) {
return LocationAlarm.fromJson(json);
}
throw ArgumentError('Unknown alarm type');
}
@override
bool operator ==(Object other) {
if (other is! Alarm) return false;
return hashCode == other.hashCode;
}
@override
int get hashCode => taskId.hashCode;
}
-43
View File
@@ -1,43 +0,0 @@
import 'fixed_time_alarm.dart';
import 'location_alarm.dart';
import 'relative_time_alarm.dart';
abstract class Alarm {
String get id;
AlarmType get alarmType;
Map<String, dynamic> toJson();
factory Alarm.fromJson(Map<String, dynamic> json) {
if (json.containsKey('alarmType')) {
switch (AlarmType.fromJson(json['alarmType'])) {
case AlarmType.timeFixed:
return FixedTimeAlarm.fromJson(json);
case AlarmType.timeRelative:
return RelativeTimeAlarm.fromJson(json);
case AlarmType.location:
return LocationAlarm.fromJson(json);
}
}
throw ArgumentError('Unknown alarm type');
}
@override
bool operator ==(Object other) {
if (other is! Alarm) return false;
return hashCode == other.hashCode;
}
@override
int get hashCode => id.hashCode;
}
enum AlarmType {
timeFixed,
timeRelative,
location;
String toJson() => name;
static AlarmType fromJson(String json) => values.byName(json);
}
-30
View File
@@ -1,30 +0,0 @@
import 'alarm.dart';
class RelativeTimeAlarm implements Alarm {
@override
final String id;
final DateTime triggerAt;
@override
final AlarmType alarmType;
const RelativeTimeAlarm({required this.id, required this.triggerAt})
: alarmType = AlarmType.timeRelative;
factory RelativeTimeAlarm.fromJson(Map<String, dynamic> json) {
return RelativeTimeAlarm(
id: json['id'] as String,
triggerAt: DateTime.parse(json['triggerAt'] as String),
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'triggerAt': triggerAt.toIso8601String(),
'alarmType': alarmType.toJson(),
};
}
}
@@ -1,4 +1,3 @@
import '../alarm/alarm.dart';
import '../task.dart'; import '../task.dart';
class CreateTaskRequest { class CreateTaskRequest {
@@ -10,7 +9,6 @@ class CreateTaskRequest {
final String category; final String category;
final List<Task> subtasks; final List<Task> subtasks;
final String url; final String url;
final List<Alarm> alarms;
CreateTaskRequest({ CreateTaskRequest({
required this.title, required this.title,
@@ -21,7 +19,6 @@ class CreateTaskRequest {
required this.category, required this.category,
required this.subtasks, required this.subtasks,
required this.url, required this.url,
required this.alarms,
}); });
CreateTaskRequest.fromTask(Task task) CreateTaskRequest.fromTask(Task task)
@@ -32,8 +29,7 @@ class CreateTaskRequest {
isCompleted = task.isCompleted, isCompleted = task.isCompleted,
category = task.category, category = task.category,
subtasks = task.subtasks, subtasks = task.subtasks,
url = task.url, url = task.url;
alarms = task.alarms;
Task toTask({required String id}) { Task toTask({required String id}) {
return Task( return Task(
@@ -46,7 +42,6 @@ class CreateTaskRequest {
category: category, category: category,
subtasks: subtasks, subtasks: subtasks,
url: url, url: url,
alarms: alarms,
); );
} }
} }
@@ -1,26 +1,28 @@
import 'alarm.dart'; import 'alarm.dart';
import '../location.dart'; import 'location.dart';
class LocationAlarm implements Alarm { class LocationAlarm implements Alarm {
@override @override
final String id; final String id;
@override
final String taskId;
final Location location; final Location location;
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.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(
id: json['id'] as String, id: json['id'] as String,
taskId: json['taskId'] as String,
location: Location.fromJson(json['location'] as Map<String, dynamic>), location: Location.fromJson(json['location'] as Map<String, dynamic>),
radiusMeters: json['radiusMeters'] as int, radiusMeters: json['radiusMeters'] as int,
); );
@@ -30,9 +32,9 @@ class LocationAlarm implements Alarm {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
'taskId': taskId,
'location': location.toJson(), 'location': location.toJson(),
'radiusMeters': radiusMeters, 'radiusMeters': radiusMeters,
'alarmType': alarmType.toJson(),
}; };
} }
} }
@@ -1,4 +1,4 @@
import '../../model/alarm/alarm.dart'; import '../../alarm.dart';
abstract class AlarmRepository { abstract class AlarmRepository {
// Create // Create
@@ -1,4 +1,4 @@
import '../../model/location.dart'; import '../../location.dart';
abstract class LocationRepository { abstract class LocationRepository {
// Create // Create
@@ -1,4 +1,4 @@
import '../../model/task.dart'; import '../../task.dart';
abstract class TaskRepository { abstract class TaskRepository {
// Create // Create
@@ -2,9 +2,9 @@ import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../model/alarm/alarm.dart'; import '../alarm.dart';
import '../model/location.dart'; import '../location.dart';
import '../model/task.dart'; import '../task.dart';
import 'interfaces/alarm_repository.dart'; import 'interfaces/alarm_repository.dart';
import 'interfaces/location_repository.dart'; import 'interfaces/location_repository.dart';
import 'interfaces/task_repository.dart'; import 'interfaces/task_repository.dart';
@@ -1,9 +0,0 @@
import '../alarm/alarm.dart' show AlarmType, Alarm;
abstract class CreateAlarmRequest {
const CreateAlarmRequest({required this.alarmType});
final AlarmType alarmType;
Alarm toAlarm(String id);
}
@@ -1,18 +0,0 @@
import '../alarm/alarm.dart';
import '../alarm/fixed_time_alarm.dart';
import 'create_alarm_request.dart';
class CreateFixedTimeAlarmRequest extends CreateAlarmRequest {
final DateTime? triggerAt;
const CreateFixedTimeAlarmRequest({this.triggerAt})
: super(alarmType: AlarmType.timeFixed);
CreateFixedTimeAlarmRequest.fromAlarm(FixedTimeAlarm alarm)
: triggerAt = alarm.triggerAt,
super(alarmType: AlarmType.timeFixed);
@override
FixedTimeAlarm toAlarm(String id) =>
FixedTimeAlarm(id: id, triggerAt: triggerAt!);
}
@@ -1,22 +0,0 @@
import '../alarm/alarm.dart' show AlarmType;
import '../alarm/location_alarm.dart';
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);
CreateLocationAlarmRequest.fromAlarm(LocationAlarm alarm)
: location = alarm.location,
radiusMeters = alarm.radiusMeters,
super(alarmType: AlarmType.location);
@override
LocationAlarm toAlarm(String id) =>
LocationAlarm(id: id, location: location!, radiusMeters: radiusMeters!);
}
@@ -1,18 +0,0 @@
import '../alarm/alarm.dart' show AlarmType;
import '../alarm/relative_time_alarm.dart';
import 'create_alarm_request.dart';
class CreateRelativeTimeAlarmRequest extends CreateAlarmRequest {
final DateTime? triggerAt;
const CreateRelativeTimeAlarmRequest({this.triggerAt})
: super(alarmType: AlarmType.timeRelative);
CreateRelativeTimeAlarmRequest.fromAlarm(RelativeTimeAlarm alarm)
: triggerAt = alarm.triggerAt,
super(alarmType: AlarmType.timeRelative);
@override
RelativeTimeAlarm toAlarm(String id) =>
RelativeTimeAlarm(id: id, triggerAt: triggerAt!);
}
+1 -11
View File
@@ -1,4 +1,3 @@
import 'alarm/alarm.dart';
import 'location.dart'; import 'location.dart';
class Task { class Task {
@@ -11,7 +10,6 @@ class Task {
final String category; final String category;
final List<Task> subtasks; final List<Task> subtasks;
final String url; final String url;
final List<Alarm> alarms;
Task({ Task({
required this.id, required this.id,
@@ -23,7 +21,6 @@ class Task {
this.category = '', this.category = '',
this.subtasks = const [], this.subtasks = const [],
this.url = '', this.url = '',
required this.alarms,
}); });
Task copyWith({ Task copyWith({
@@ -35,7 +32,7 @@ class Task {
bool? isCompleted, bool? isCompleted,
String? category, String? category,
List<Task>? subtasks, List<Task>? subtasks,
List<Alarm>? alarms, List<DateTime>? alarms,
Location? location, Location? location,
String? url, String? url,
}) { }) {
@@ -49,7 +46,6 @@ class Task {
category: category ?? this.category, category: category ?? this.category,
subtasks: subtasks ?? this.subtasks, subtasks: subtasks ?? this.subtasks,
url: url ?? this.url, url: url ?? this.url,
alarms: alarms ?? this.alarms,
); );
} }
@@ -70,11 +66,6 @@ class Task {
.toList() ?? .toList() ??
[], [],
url: json['url'] as String? ?? '', url: json['url'] as String? ?? '',
alarms:
(json['alarms'] as List<dynamic>?)
?.map((e) => Alarm.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
); );
} }
@@ -89,7 +80,6 @@ class Task {
'category': category, 'category': category,
'subtasks': subtasks.map((e) => e.toJson()).toList(), 'subtasks': subtasks.map((e) => e.toJson()).toList(),
'url': url, 'url': url,
'alarms': alarms.map((e) => e.toJson()).toList(),
}; };
} }
@@ -1,20 +1,24 @@
import 'alarm.dart'; import 'alarm.dart';
class FixedTimeAlarm implements Alarm { class TimeAlarm implements Alarm {
@override @override
final String id; final String id;
@override
final String taskId;
final DateTime triggerAt; final DateTime triggerAt;
@override const TimeAlarm({
final AlarmType alarmType; required this.id,
required this.taskId,
required this.triggerAt,
});
const FixedTimeAlarm({required this.id, required this.triggerAt}) factory TimeAlarm.fromJson(Map<String, dynamic> json) {
: alarmType = AlarmType.timeFixed; return TimeAlarm(
factory FixedTimeAlarm.fromJson(Map<String, dynamic> json) {
return FixedTimeAlarm(
id: json['id'] as String, id: json['id'] as String,
taskId: json['taskId'] as String,
triggerAt: DateTime.parse(json['triggerAt'] as String), triggerAt: DateTime.parse(json['triggerAt'] as String),
); );
} }
@@ -23,8 +27,8 @@ class FixedTimeAlarm implements Alarm {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
'taskId': taskId,
'triggerAt': triggerAt.toIso8601String(), 'triggerAt': triggerAt.toIso8601String(),
'alarmType': alarmType.toJson(),
}; };
} }
} }
+1 -86
View File
@@ -1,20 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../app_theme.dart'; import '../app_theme.dart';
import '../model/alarm/alarm.dart'; import '../model/callback_models/create_task_request.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_fixed_time_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/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 {
@@ -41,7 +32,6 @@ 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> alarmRequests = [];
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@@ -60,18 +50,6 @@ 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;
} }
} }
@@ -102,7 +80,6 @@ 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: SingleChildScrollView(
child: Column( child: Column(
spacing: AppTheme.formColumnSpacing, spacing: AppTheme.formColumnSpacing,
children: [ children: [
@@ -142,45 +119,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
textInputAction: TextInputAction.next, 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, alarmRequest) in alarmRequests.indexed)
AlarmForm(
request: alarmRequest,
onChanged: (updated) {
setState(() {
alarmRequests[index] = updated;
});
},
),
], ],
), ),
],
),
),
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
@@ -203,18 +143,6 @@ class _TaskEditPageState extends State<TaskEditPage> {
category: categoryController.text, category: categoryController.text,
subtasks: [], subtasks: [],
url: urlController.text, url: urlController.text,
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(),
), ),
); );
} }
@@ -246,17 +174,4 @@ class _TaskEditPageState extends State<TaskEditPage> {
}); });
} }
} }
void onAddAlarmPressed(AlarmType alarmType) {
switch (alarmType) {
case AlarmType.location:
alarmRequests.add(CreateLocationAlarmRequest());
case AlarmType.timeFixed:
alarmRequests.add(CreateFixedTimeAlarmRequest());
case AlarmType.timeRelative:
alarmRequests.add(CreateFixedTimeAlarmRequest());
}
setState(() {});
}
} }
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../model/requests/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';
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../model/alarm/alarm.dart'; import '../../model/alarm.dart';
import '../../repositories/interfaces/alarm_repository.dart'; import '../../model/repositories/interfaces/alarm_repository.dart';
class AlarmController extends ChangeNotifier { class AlarmController extends ChangeNotifier {
AlarmController(AlarmRepository repository) : _repository = repository { AlarmController(AlarmRepository repository) : _repository = repository {
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../model/location.dart'; import '../../model/location.dart';
import '../../repositories/interfaces/location_repository.dart'; import '../../model/repositories/interfaces/location_repository.dart';
class LocationController extends ChangeNotifier { class LocationController extends ChangeNotifier {
LocationController(LocationRepository repository) : _repository = repository { LocationController(LocationRepository repository) : _repository = repository {
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart' show ChangeNotifier; import 'package:flutter/material.dart' show ChangeNotifier;
import '../../repositories/interfaces/task_repository.dart'; import '../../model/repositories/interfaces/task_repository.dart';
import '../../model/task.dart'; import '../../model/task.dart';
class TaskController extends ChangeNotifier { class TaskController extends ChangeNotifier {
-9
View File
@@ -7,15 +7,6 @@ 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;
@@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import '../../model/alarm/alarm.dart' show AlarmType;
import '../../model/requests/create_alarm_request.dart';
import '../../model/requests/create_fixed_time_alarm_request.dart';
import '../../model/requests/create_location_alarm_request.dart';
import '../../model/requests/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,
);
}
}
}
@@ -1,168 +0,0 @@
import 'package:flutter/material.dart';
import '../../app_theme.dart' show AppTheme;
import '../../model/requests/create_fixed_time_alarm_request.dart';
import '../../service/tools.dart';
import '../../service/validators.dart';
class FixedTimeAlarmForm extends StatefulWidget {
const FixedTimeAlarmForm({
super.key,
required this.request,
required this.onChanged,
});
final CreateFixedTimeAlarmRequest request;
final ValueChanged<CreateFixedTimeAlarmRequest> onChanged;
@override
State<FixedTimeAlarmForm> createState() => _FixedTimeAlarmFormState();
}
class _FixedTimeAlarmFormState extends State<FixedTimeAlarmForm> {
final dueDateController = TextEditingController();
final dueTimeController = TextEditingController();
final dueDateFocusNode = FocusNode();
final dueTimeFocusNode = FocusNode();
bool isDueTimeEnabled = false;
@override
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);
dueTimeController.addListener(onDueTimeChanged);
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Flex(
direction: Axis.horizontal,
children: [
Flexible(
flex: 3,
child: TextFormField(
focusNode: dueDateFocusNode,
controller: dueDateController,
onFieldSubmitted: (_) {
isDueTimeEnabled ? dueDateFocusNode.nextFocus() : null;
},
decoration: InputDecoration(
label: Text('Due Date'),
suffixIcon: IconButton(
onPressed: () async {
final result = await onOpenCalendarPickerPressed();
if (result != null) {
final dateString = getIsoDateString(result);
dueDateController.text = dateString;
maybeEnableDueTime(dateString);
dueTimeFocusNode.requestFocus();
}
},
icon: Icon(Icons.calendar_month),
),
),
validator: dateTimeValidator,
keyboardType: TextInputType.datetime,
textInputAction: TextInputAction.next,
),
),
Padding(
padding: EdgeInsetsGeometry.only(left: AppTheme.formColumnSpacing),
),
Flexible(
flex: 2,
child: TextFormField(
focusNode: dueTimeFocusNode,
onFieldSubmitted: (_) => dueTimeFocusNode.nextFocus(),
controller: dueTimeController,
enabled: isDueTimeEnabled,
onChanged: (value) {},
decoration: InputDecoration(
label: Text('Due Time'),
suffixIcon: IconButton(
onPressed: () async {
final result = await onOpenTimePickerPressed();
if (result != null && context.mounted) {
dueTimeController.text = result.format(context);
}
},
icon: Icon(Icons.schedule),
),
),
validator: timeValidator,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
),
),
],
);
}
Future<DateTime?> onOpenCalendarPickerPressed() {
return showDialog<DateTime?>(
context: context,
builder: (context) => DatePickerDialog(
firstDate: DateTime(DateTime.now().year - 100),
lastDate: DateTime(DateTime.now().year + 100),
initialDate:
DateTime.tryParse(dueDateController.text) ?? DateTime.now(),
),
);
}
Future<TimeOfDay?> onOpenTimePickerPressed() {
return showDialog<TimeOfDay?>(
context: context,
builder: (context) => TimePickerDialog(
initialTime: TimeOfDay.fromDateTime(
widget.request.triggerAt ?? DateTime.now(),
),
),
);
}
void maybeEnableDueTime(String value) {
if (value.isNotEmpty && dateTimeValidator(value) == null) {
setState(() {
isDueTimeEnabled = true;
});
} else if (isDueTimeEnabled) {
setState(() {
isDueTimeEnabled = false;
});
}
}
void onDueDateChanged() {
final String value = dueDateController.text;
maybeEnableDueTime(value);
if (dateTimeValidator(value) == null) {
final dateTime = DateTime.tryParse(
'$value ${dueTimeController.text}'.trim(),
);
if (dateTime != null) {
widget.onChanged(CreateFixedTimeAlarmRequest(triggerAt: dateTime));
}
}
}
void onDueTimeChanged() {
final String value = dueTimeController.text;
if (timeValidator(value) == null) {
final dateTime = DateTime.tryParse(
'${dueDateController.text} $value'.trim(),
);
if (dateTime != null) {
widget.onChanged(CreateFixedTimeAlarmRequest(triggerAt: dateTime));
}
}
}
}
@@ -1,19 +0,0 @@
import 'package:flutter/material.dart';
import '../../model/requests/create_alarm_request.dart';
import '../../model/requests/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();
}
}
@@ -1,19 +0,0 @@
import 'package:flutter/material.dart';
import '../../model/requests/create_alarm_request.dart';
import '../../model/requests/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();
}
}
+2 -28
View File
@@ -1,6 +1,5 @@
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';
@@ -12,7 +11,6 @@ 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;
@@ -20,7 +18,6 @@ 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();
@@ -50,18 +47,7 @@ class _TimeSelectorState extends State<TimeSelector> {
child: TextFormField( child: TextFormField(
focusNode: dueDateFocusNode, focusNode: dueDateFocusNode,
controller: widget.dueDateController, controller: widget.dueDateController,
onChanged: (value) { onChanged: maybeEnableDueTime,
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()
@@ -88,9 +74,7 @@ class _TimeSelectorState extends State<TimeSelector> {
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
), ),
), ),
Padding( Padding(padding: EdgeInsetsGeometry.only(left: 10)),
padding: EdgeInsetsGeometry.only(left: AppTheme.formColumnSpacing),
),
Flexible( Flexible(
flex: 2, flex: 2,
child: TextFormField( child: TextFormField(
@@ -98,16 +82,6 @@ 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(