Compare commits
44
Commits
v0.1.1
...
e6951b4c25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6951b4c25 | ||
|
|
cd97dada2a | ||
|
|
6778b6426f | ||
|
|
1b9e096b77 | ||
|
|
8113ce9c22 | ||
|
|
06de421ee2 | ||
|
|
e7d6735d27 | ||
|
|
79cae9570d | ||
|
|
7fed7bfe16 | ||
|
|
02469faf52 | ||
|
|
3ceb0896a4 | ||
|
|
c7ff39e55c | ||
|
|
a33c4bbb12 | ||
|
|
5fc290dbcb | ||
|
|
463a2c8fda | ||
|
|
dcae40497d | ||
|
|
a440c29043 | ||
|
|
e1eafdca59 | ||
|
|
4af3683b73 | ||
|
|
505aff4232 | ||
|
|
21ef9c4ab5 | ||
|
|
9d0cb7668d | ||
|
|
383de6a33b | ||
|
|
9e950a1767 | ||
|
|
20a7c88d2f | ||
|
|
e8057a8cc6 | ||
|
|
a71cc454eb | ||
|
|
f1c1578620 | ||
|
|
5ef22c7b50 | ||
|
|
6dc7161b41 | ||
|
|
f1756b30d1 | ||
|
|
77a524f3ec | ||
|
|
064c014f8b | ||
|
|
410a7eb843 | ||
|
|
20b017b066 | ||
|
|
a3258b84fe | ||
|
|
f088b84c54 | ||
|
|
5a39148577 | ||
|
|
2b718b5bd8 | ||
|
|
023610804d | ||
|
|
cee5af0f84 | ||
|
|
999023e48a | ||
|
|
82a74a66b5 | ||
|
|
e645081204 |
@@ -0,0 +1,59 @@
|
||||
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,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
);
|
||||
|
||||
static ThemeData get darkTheme => _baseTheme(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.indigo,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
static ThemeData _baseTheme({required ColorScheme colorScheme}) {
|
||||
final theme = ThemeData(useMaterial3: true, colorScheme: colorScheme);
|
||||
final universalBorderRadius = BorderRadius.circular(12);
|
||||
|
||||
return theme.copyWith(
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: OutlineInputBorder(borderRadius: universalBorderRadius),
|
||||
),
|
||||
|
||||
listTileTheme: ListTileThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: universalBorderRadius,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+17
-4
@@ -1,10 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'model/repositories/local_repository.dart';
|
||||
import 'app_theme.dart';
|
||||
import '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';
|
||||
import 'service/task_controller.dart';
|
||||
import 'service/controllers/alarm_controller.dart';
|
||||
import 'service/controllers/location_controller.dart';
|
||||
import 'service/controllers/task_controller.dart';
|
||||
|
||||
void main() async {
|
||||
final repository = LocalRepository();
|
||||
@@ -13,8 +17,14 @@ void main() async {
|
||||
|
||||
runApp(
|
||||
ControllerScope(
|
||||
controller: TaskController(repository),
|
||||
child: const MainApp(),
|
||||
controller: LocationController(repository),
|
||||
child: ControllerScope(
|
||||
controller: AlarmController(repository),
|
||||
child: ControllerScope(
|
||||
controller: TaskController(repository),
|
||||
child: const MainApp(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -25,9 +35,12 @@ class MainApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
routes: {
|
||||
TaskOverviewPage.routeName: (context) => TaskOverviewPage(),
|
||||
TaskEditPage.routeName: (context) => TaskEditPage(),
|
||||
LocationsOverviewPage.routeName: (context) => LocationsOverviewPage(),
|
||||
},
|
||||
initialRoute: TaskOverviewPage.routeName,
|
||||
);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'location_alarm.dart';
|
||||
import 'time_alarm.dart';
|
||||
|
||||
abstract class Alarm {
|
||||
String get id;
|
||||
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 => id.hashCode;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'alarm.dart';
|
||||
|
||||
class FixedTimeAlarm implements Alarm {
|
||||
@override
|
||||
final String id;
|
||||
|
||||
final DateTime triggerAt;
|
||||
|
||||
@override
|
||||
final AlarmType alarmType;
|
||||
|
||||
const FixedTimeAlarm({required this.id, required this.triggerAt})
|
||||
: alarmType = AlarmType.timeFixed;
|
||||
|
||||
factory FixedTimeAlarm.fromJson(Map<String, dynamic> json) {
|
||||
return FixedTimeAlarm(
|
||||
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,5 +1,5 @@
|
||||
import 'alarm.dart';
|
||||
import 'location.dart';
|
||||
import '../location.dart';
|
||||
|
||||
class LocationAlarm implements Alarm {
|
||||
@override
|
||||
@@ -9,11 +9,14 @@ class LocationAlarm implements Alarm {
|
||||
|
||||
final int radiusMeters;
|
||||
|
||||
@override
|
||||
final AlarmType alarmType;
|
||||
|
||||
const LocationAlarm({
|
||||
required this.id,
|
||||
required this.location,
|
||||
required this.radiusMeters,
|
||||
});
|
||||
}) : alarmType = AlarmType.location;
|
||||
|
||||
factory LocationAlarm.fromJson(Map<String, dynamic> json) {
|
||||
return LocationAlarm(
|
||||
@@ -29,6 +32,7 @@ class LocationAlarm implements Alarm {
|
||||
'id': id,
|
||||
'location': location.toJson(),
|
||||
'radiusMeters': radiusMeters,
|
||||
'alarmType': alarmType.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> json) {
|
||||
return LatLng(json['lat'] as double, json['lng'] as double);
|
||||
}
|
||||
@@ -12,4 +21,18 @@ class LatLng {
|
||||
Map<String, double> 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()}';
|
||||
}
|
||||
}
|
||||
|
||||
+17
-2
@@ -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<String, dynamic> json) {
|
||||
return Location(
|
||||
name: json['name'] as String,
|
||||
address: json['address'] as String,
|
||||
coordinates: LatLng.fromJson(json['coordinates']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import '../alarm/alarm.dart' show AlarmType, Alarm;
|
||||
|
||||
abstract class CreateAlarmRequest {
|
||||
const CreateAlarmRequest({required this.alarmType});
|
||||
|
||||
final AlarmType alarmType;
|
||||
|
||||
Alarm toAlarm(String id);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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!);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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!);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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!);
|
||||
}
|
||||
+6
-10
@@ -1,4 +1,4 @@
|
||||
import '../location.dart';
|
||||
import '../alarm/alarm.dart';
|
||||
import '../task.dart';
|
||||
|
||||
class CreateTaskRequest {
|
||||
@@ -9,9 +9,8 @@ class CreateTaskRequest {
|
||||
final bool isCompleted;
|
||||
final String category;
|
||||
final List<Task> subtasks;
|
||||
final List<DateTime> alarms;
|
||||
final Location? location;
|
||||
final String url;
|
||||
final List<Alarm> alarms;
|
||||
|
||||
CreateTaskRequest({
|
||||
required this.title,
|
||||
@@ -21,9 +20,8 @@ class CreateTaskRequest {
|
||||
required this.isCompleted,
|
||||
required this.category,
|
||||
required this.subtasks,
|
||||
required this.alarms,
|
||||
required this.location,
|
||||
required this.url,
|
||||
required this.alarms,
|
||||
});
|
||||
|
||||
CreateTaskRequest.fromTask(Task task)
|
||||
@@ -34,9 +32,8 @@ class CreateTaskRequest {
|
||||
isCompleted = task.isCompleted,
|
||||
category = task.category,
|
||||
subtasks = task.subtasks,
|
||||
alarms = task.alarms,
|
||||
location = task.location,
|
||||
url = task.url;
|
||||
url = task.url,
|
||||
alarms = task.alarms;
|
||||
|
||||
Task toTask({required String id}) {
|
||||
return Task(
|
||||
@@ -48,9 +45,8 @@ class CreateTaskRequest {
|
||||
isCompleted: isCompleted,
|
||||
category: category,
|
||||
subtasks: subtasks,
|
||||
alarms: alarms,
|
||||
location: location,
|
||||
url: url,
|
||||
alarms: alarms,
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
-14
@@ -1,3 +1,4 @@
|
||||
import 'alarm/alarm.dart';
|
||||
import 'location.dart';
|
||||
|
||||
class Task {
|
||||
@@ -9,9 +10,8 @@ class Task {
|
||||
final bool isCompleted;
|
||||
final String category;
|
||||
final List<Task> subtasks;
|
||||
final List<DateTime> alarms;
|
||||
final Location? location;
|
||||
final String url;
|
||||
final List<Alarm> alarms;
|
||||
|
||||
Task({
|
||||
required this.id,
|
||||
@@ -22,9 +22,8 @@ class Task {
|
||||
this.isCompleted = false,
|
||||
this.category = '',
|
||||
this.subtasks = const [],
|
||||
this.alarms = const [],
|
||||
this.location,
|
||||
this.url = '',
|
||||
required this.alarms,
|
||||
});
|
||||
|
||||
Task copyWith({
|
||||
@@ -36,7 +35,7 @@ class Task {
|
||||
bool? isCompleted,
|
||||
String? category,
|
||||
List<Task>? subtasks,
|
||||
List<DateTime>? alarms,
|
||||
List<Alarm>? alarms,
|
||||
Location? location,
|
||||
String? url,
|
||||
}) {
|
||||
@@ -49,9 +48,8 @@ class Task {
|
||||
isCompleted: isCompleted ?? this.isCompleted,
|
||||
category: category ?? this.category,
|
||||
subtasks: subtasks ?? this.subtasks,
|
||||
alarms: alarms ?? this.alarms,
|
||||
location: location ?? this.location,
|
||||
url: url ?? this.url,
|
||||
alarms: alarms ?? this.alarms,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,15 +69,12 @@ class Task {
|
||||
?.map((e) => Task.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
url: json['url'] as String? ?? '',
|
||||
alarms:
|
||||
(json['alarms'] as List<dynamic>?)
|
||||
?.map((e) => DateTime.parse(e as String))
|
||||
?.map((e) => Alarm.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
location: json['location'] != null
|
||||
? Location.fromJson(json['location'] as Map<String, dynamic>)
|
||||
: null,
|
||||
url: json['url'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,9 +88,8 @@ class Task {
|
||||
'isCompleted': isCompleted,
|
||||
'category': category,
|
||||
'subtasks': subtasks.map((e) => e.toJson()).toList(),
|
||||
'alarms': alarms.map((e) => e.toIso8601String()).toList(),
|
||||
'location': location?.toJson(),
|
||||
'url': url,
|
||||
'alarms': alarms.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'alarm.dart';
|
||||
|
||||
class TimeAlarm implements Alarm {
|
||||
@override
|
||||
final String id;
|
||||
|
||||
final DateTime triggerAt;
|
||||
|
||||
const TimeAlarm({required this.id, required this.triggerAt});
|
||||
|
||||
factory TimeAlarm.fromJson(Map<String, dynamic> json) {
|
||||
return TimeAlarm(
|
||||
id: json['id'] as String,
|
||||
triggerAt: DateTime.parse(json['triggerAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'id': id, 'triggerAt': triggerAt.toIso8601String()};
|
||||
}
|
||||
}
|
||||
@@ -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<LocationsOverviewPage> createState() => _LocationsOverviewPageState();
|
||||
}
|
||||
|
||||
class _LocationsOverviewPageState extends State<LocationsOverviewPage> {
|
||||
List<Location> locations = [];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
locations = context.controller<LocationController>().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<LocationController>().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<Location?>(
|
||||
context: context,
|
||||
builder: (context) => CreateLocationDialog(),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
if (mounted && result != null) {
|
||||
context.controller<LocationController>().addLocation(result);
|
||||
}
|
||||
}
|
||||
|
||||
void onEditLocationButtonPressed(Location location) async {
|
||||
final result = await showDialog<Location?>(
|
||||
context: context,
|
||||
builder: (context) => CreateLocationDialog(initialLocation: location),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
if (mounted && result != null) {
|
||||
context.controller<LocationController>().updateLocation(location, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
+129
-43
@@ -1,10 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../model/callback_models/create_task_request.dart';
|
||||
import '../app_theme.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_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/task.dart';
|
||||
import '../service/task_controller.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 {
|
||||
@@ -31,6 +41,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
final formKey = GlobalKey<FormState>(debugLabel: 'taskEditFormKey');
|
||||
bool didFormChange = false;
|
||||
bool isDueTimeEnabled = false;
|
||||
final List<CreateAlarmRequest> alarmRequests = [];
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -49,6 +60,18 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
isDueTimeEnabled = 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;
|
||||
}
|
||||
}
|
||||
@@ -79,44 +102,84 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width * 0.05,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
controller: titleController,
|
||||
decoration: InputDecoration(label: Text('Title')),
|
||||
keyboardType: TextInputType.text,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(label: Text('Description')),
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
minLines: 3,
|
||||
maxLines: 10,
|
||||
),
|
||||
TimeSelector(
|
||||
initialDueDateTime: task?.due,
|
||||
nextFocusNode: categoryFocusNode,
|
||||
dueDateController: dueDateController,
|
||||
dueTimeController: dueTimeController,
|
||||
formKey: formKey,
|
||||
),
|
||||
TextFormField(
|
||||
focusNode: categoryFocusNode,
|
||||
controller: categoryController,
|
||||
decoration: InputDecoration(label: Text('Category')),
|
||||
keyboardType: TextInputType.text,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
TextFormField(
|
||||
controller: urlController,
|
||||
decoration: InputDecoration(label: Text('Url')),
|
||||
keyboardType: TextInputType.text,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: AppTheme.formColumnSpacing,
|
||||
children: [
|
||||
SizedBox(height: 6),
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
controller: titleController,
|
||||
decoration: InputDecoration(label: Text('Title')),
|
||||
keyboardType: TextInputType.text,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(label: Text('Description')),
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
minLines: 3,
|
||||
maxLines: 10,
|
||||
),
|
||||
TimeSelector(
|
||||
initialDueDateTime: task?.due,
|
||||
nextFocusNode: categoryFocusNode,
|
||||
dueDateController: dueDateController,
|
||||
dueTimeController: dueTimeController,
|
||||
formKey: formKey,
|
||||
),
|
||||
TextFormField(
|
||||
focusNode: categoryFocusNode,
|
||||
controller: categoryController,
|
||||
decoration: InputDecoration(label: Text('Category')),
|
||||
keyboardType: TextInputType.text,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
TextFormField(
|
||||
controller: urlController,
|
||||
decoration: InputDecoration(label: Text('Url')),
|
||||
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, alarmRequest) in alarmRequests.indexed)
|
||||
AlarmForm(
|
||||
request: alarmRequest,
|
||||
onChanged: (updated) {
|
||||
setState(() {
|
||||
alarmRequests[index] = updated;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -139,9 +202,19 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
isCompleted: false,
|
||||
category: categoryController.text,
|
||||
subtasks: [],
|
||||
alarms: [],
|
||||
location: null,
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -167,10 +240,23 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
],
|
||||
),
|
||||
).then((result) {
|
||||
if (result != null && result && context.mounted) {
|
||||
if (result != null && result && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../model/callback_models/create_task_request.dart';
|
||||
import '../model/requests/create_task_request.dart';
|
||||
import '../model/extensions/controller_context.dart';
|
||||
import '../model/task.dart';
|
||||
import '../service/task_controller.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 {
|
||||
@@ -21,11 +23,29 @@ class _TaskOverviewPageState extends State<TaskOverviewPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: ReorderableListView.builder(
|
||||
itemBuilder: itemBuilder,
|
||||
itemCount: tasks.length,
|
||||
onReorderItem: context.controller<TaskController>().reorderTask,
|
||||
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,
|
||||
),
|
||||
child: ReorderableListView.builder(
|
||||
itemBuilder: itemBuilder,
|
||||
itemCount: tasks.length,
|
||||
onReorderItem: context.controller<TaskController>().reorderTask,
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: onCreateTaskTapped,
|
||||
@@ -37,24 +57,32 @@ class _TaskOverviewPageState extends State<TaskOverviewPage> {
|
||||
Widget itemBuilder(BuildContext context, int index) {
|
||||
final task = tasks.elementAt(index);
|
||||
|
||||
return ListTile(
|
||||
return Padding(
|
||||
key: Key(task.id),
|
||||
title: Text(task.title),
|
||||
subtitle: task.description.isNotEmpty ? Text(task.description) : null,
|
||||
trailing: Checkbox(
|
||||
value: task.isCompleted,
|
||||
onChanged: (isCompleted) => context
|
||||
.controller<TaskController>()
|
||||
.saveTask(task.copyWith(isCompleted: isCompleted)),
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: TaskDismissible(
|
||||
key: Key(task.id),
|
||||
onDismissedRight: () =>
|
||||
context.controller<TaskController>().deleteTask(task),
|
||||
child: ListTile(
|
||||
title: Text(task.title),
|
||||
subtitle: task.description.isNotEmpty ? Text(task.description) : null,
|
||||
trailing: Checkbox(
|
||||
value: task.isCompleted,
|
||||
onChanged: (isCompleted) => context
|
||||
.controller<TaskController>()
|
||||
.saveTask(task.copyWith(isCompleted: isCompleted)),
|
||||
),
|
||||
onTap: () async {
|
||||
final result = await onTaskTapped(task);
|
||||
if (result != null && context.mounted) {
|
||||
context.controller<TaskController>().saveTask(
|
||||
result.toTask(id: task.id),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final result = await onTaskTapped(task);
|
||||
if (result != null && context.mounted) {
|
||||
context.controller<TaskController>().saveTask(
|
||||
result.toTask(id: task.id),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,10 +98,13 @@ class _TaskOverviewPageState extends State<TaskOverviewPage> {
|
||||
await Navigator.of(context).pushNamed(TaskEditPage.routeName)
|
||||
as CreateTaskRequest?;
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
if (result != null && mounted) {
|
||||
context.controller<TaskController>().saveTask(
|
||||
result.toTask(id: generateId()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onLocationsButtonTapped() =>
|
||||
Navigator.of(context).pushNamed(LocationsOverviewPage.routeName);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import '../../alarm.dart';
|
||||
import '../../model/alarm/alarm.dart';
|
||||
|
||||
abstract class AlarmRepository {
|
||||
// Create
|
||||
@@ -0,0 +1,19 @@
|
||||
import '../../model/location.dart';
|
||||
|
||||
abstract class LocationRepository {
|
||||
// Create
|
||||
|
||||
Future<void> createLocation(Location location);
|
||||
|
||||
// Read
|
||||
|
||||
Future<List<Location>> loadLocations();
|
||||
|
||||
// Update
|
||||
|
||||
Future<void> updateLocation(Location location);
|
||||
|
||||
// Delete
|
||||
|
||||
Future<void> deleteLocation(Location location);
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import '../../task.dart';
|
||||
import '../../model/task.dart';
|
||||
|
||||
abstract class TaskRepository {
|
||||
// Create
|
||||
+51
-4
@@ -2,15 +2,19 @@ import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../alarm.dart';
|
||||
import '../task.dart';
|
||||
import '../model/alarm/alarm.dart';
|
||||
import '../model/location.dart';
|
||||
import '../model/task.dart';
|
||||
import 'interfaces/alarm_repository.dart';
|
||||
import 'interfaces/location_repository.dart';
|
||||
import 'interfaces/task_repository.dart';
|
||||
|
||||
class LocalRepository implements TaskRepository, AlarmRepository {
|
||||
class LocalRepository
|
||||
implements TaskRepository, AlarmRepository, LocationRepository {
|
||||
static const String _tasksKey = 'tasks';
|
||||
static const String _taskOrderKey = 'taskOrder';
|
||||
static const String _alarmsKey = 'alarms';
|
||||
static const String _locationsKey = 'locations';
|
||||
|
||||
SharedPreferencesWithCache? _prefs;
|
||||
|
||||
@@ -18,7 +22,12 @@ class LocalRepository implements TaskRepository, AlarmRepository {
|
||||
if (_prefs == null) {
|
||||
await SharedPreferencesWithCache.create(
|
||||
cacheOptions: const SharedPreferencesWithCacheOptions(
|
||||
allowList: <String>{_tasksKey, _taskOrderKey},
|
||||
allowList: <String>{
|
||||
_tasksKey,
|
||||
_taskOrderKey,
|
||||
_alarmsKey,
|
||||
_locationsKey,
|
||||
},
|
||||
),
|
||||
).then((value) => _prefs = value);
|
||||
}
|
||||
@@ -29,6 +38,13 @@ class LocalRepository implements TaskRepository, AlarmRepository {
|
||||
_prefs!.setStringList(_tasksKey, jsonList);
|
||||
}
|
||||
|
||||
Future<void> _saveLocations(List<Location> locations) async {
|
||||
final jsonList = locations
|
||||
.map<String>((e) => jsonEncode(e.toJson()))
|
||||
.toList();
|
||||
_prefs!.setStringList(_locationsKey, jsonList);
|
||||
}
|
||||
|
||||
Future<void> _saveTaskOrder(List<String> taskOrder) async {
|
||||
final jsonList = taskOrder.map((e) => jsonEncode(e)).toList();
|
||||
return _prefs!.setStringList(_taskOrderKey, jsonList);
|
||||
@@ -62,6 +78,13 @@ class LocalRepository implements TaskRepository, AlarmRepository {
|
||||
_saveAlarms(alarms);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> createLocation(Location location) async {
|
||||
final locations = await loadLocations();
|
||||
locations.add(location);
|
||||
_saveLocations(locations);
|
||||
}
|
||||
|
||||
// Read
|
||||
|
||||
@override
|
||||
@@ -83,6 +106,15 @@ class LocalRepository implements TaskRepository, AlarmRepository {
|
||||
return jsonList.map<Alarm>((e) => Alarm.fromJson(jsonDecode(e))).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Location>> loadLocations() async {
|
||||
final Iterable<String> jsonList =
|
||||
_prefs!.getStringList(_locationsKey) ?? [];
|
||||
return jsonList
|
||||
.map<Location>((e) => Location.fromJson(jsonDecode(e)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Update
|
||||
|
||||
@override
|
||||
@@ -106,6 +138,14 @@ class LocalRepository implements TaskRepository, AlarmRepository {
|
||||
_saveAlarms(alarms);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateLocation(Location location) async {
|
||||
final locations = await loadLocations();
|
||||
locations.remove(location);
|
||||
locations.add(location);
|
||||
_saveLocations(locations);
|
||||
}
|
||||
|
||||
// Delete
|
||||
|
||||
@override
|
||||
@@ -128,4 +168,11 @@ class LocalRepository implements TaskRepository, AlarmRepository {
|
||||
alarms.remove(alarm);
|
||||
_saveAlarms(alarms);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteLocation(Location location) async {
|
||||
final locations = await loadLocations();
|
||||
locations.remove(location);
|
||||
_saveLocations(locations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../model/alarm/alarm.dart';
|
||||
import '../../repositories/interfaces/alarm_repository.dart';
|
||||
|
||||
class AlarmController extends ChangeNotifier {
|
||||
AlarmController(AlarmRepository repository) : _repository = repository {
|
||||
_loadAlarms();
|
||||
}
|
||||
|
||||
final AlarmRepository _repository;
|
||||
|
||||
final List<Alarm> _alarms = [];
|
||||
|
||||
Future<void> addAlarm(Alarm alarm) {
|
||||
_alarms.add(alarm);
|
||||
notifyListeners();
|
||||
return _repository.createAlarm(alarm);
|
||||
}
|
||||
|
||||
Future<void> deleteAlarm(Alarm alarm) {
|
||||
_alarms.remove(alarm);
|
||||
notifyListeners();
|
||||
return _repository.deleteAlarm(alarm);
|
||||
}
|
||||
|
||||
Future<void> _loadAlarms() {
|
||||
_alarms.clear();
|
||||
return _repository.loadAlarms().then((value) => _alarms.addAll(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../model/location.dart';
|
||||
import '../../repositories/interfaces/location_repository.dart';
|
||||
|
||||
class LocationController extends ChangeNotifier {
|
||||
LocationController(LocationRepository repository) : _repository = repository {
|
||||
_loadLocations();
|
||||
}
|
||||
|
||||
final LocationRepository _repository;
|
||||
|
||||
final List<Location> _locations = [];
|
||||
|
||||
List<Location> get locations => _locations;
|
||||
|
||||
Future<void> addLocation(Location location) {
|
||||
_locations.add(location);
|
||||
notifyListeners();
|
||||
return _repository.createLocation(location);
|
||||
}
|
||||
|
||||
Future<void> deleteLocation(Location location) {
|
||||
_locations.remove(location);
|
||||
notifyListeners();
|
||||
return _repository.deleteLocation(location);
|
||||
}
|
||||
|
||||
Future<void> 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<void> _loadLocations() {
|
||||
_locations.clear();
|
||||
return _repository.loadLocations().then(
|
||||
(value) => _locations.addAll(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart' show ChangeNotifier;
|
||||
|
||||
import '../model/repositories/interfaces/task_repository.dart';
|
||||
import '../model/task.dart';
|
||||
import '../../repositories/interfaces/task_repository.dart';
|
||||
import '../../model/task.dart';
|
||||
|
||||
class TaskController extends ChangeNotifier {
|
||||
TaskController(TaskRepository repository) : _repository = repository {
|
||||
@@ -7,8 +7,31 @@ 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;
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<CreateLocationDialog> createState() => _CreateLocationDialogState();
|
||||
}
|
||||
|
||||
class _CreateLocationDialogState extends State<CreateLocationDialog> {
|
||||
final nameController = TextEditingController();
|
||||
final addressController = TextEditingController();
|
||||
final coordinatesController = TextEditingController();
|
||||
final formKey = GlobalKey<FormState>(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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TaskDismissible extends StatelessWidget {
|
||||
const TaskDismissible({
|
||||
required super.key,
|
||||
this.onDismissedRight,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final VoidCallback? onDismissedRight;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dismissible(
|
||||
key: key!,
|
||||
direction: DismissDirection.startToEnd,
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentGeometry.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onDismissed: onDismissed,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void onDismissed(DismissDirection direction) {
|
||||
if (direction == DismissDirection.startToEnd && onDismissedRight != null) {
|
||||
onDismissedRight!();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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