diff --git a/lib/main.dart b/lib/main.dart index 0652d1f..cf4ea08 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,9 @@ import 'model/repositories/local_repository.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 +15,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(), + ), + ), ), ); } diff --git a/lib/model/alarm.dart b/lib/model/alarm.dart index f8f5844..1f73729 100644 --- a/lib/model/alarm.dart +++ b/lib/model/alarm.dart @@ -3,6 +3,7 @@ import 'time_alarm.dart'; abstract class Alarm { String get id; + String get taskId; Map toJson(); factory Alarm.fromJson(Map json) { @@ -25,5 +26,5 @@ abstract class Alarm { } @override - int get hashCode => id.hashCode; + int get hashCode => taskId.hashCode; } diff --git a/lib/model/callback_models/create_task_request.dart b/lib/model/callback_models/create_task_request.dart index 549a4ba..49c0a38 100644 --- a/lib/model/callback_models/create_task_request.dart +++ b/lib/model/callback_models/create_task_request.dart @@ -1,4 +1,3 @@ -import '../location.dart'; import '../task.dart'; class CreateTaskRequest { @@ -9,8 +8,6 @@ class CreateTaskRequest { final bool isCompleted; final String category; final List subtasks; - final List alarms; - final Location? location; final String url; CreateTaskRequest({ @@ -21,8 +18,6 @@ class CreateTaskRequest { required this.isCompleted, required this.category, required this.subtasks, - required this.alarms, - required this.location, required this.url, }); @@ -34,8 +29,6 @@ class CreateTaskRequest { isCompleted = task.isCompleted, category = task.category, subtasks = task.subtasks, - alarms = task.alarms, - location = task.location, url = task.url; Task toTask({required String id}) { @@ -48,8 +41,6 @@ class CreateTaskRequest { isCompleted: isCompleted, category: category, subtasks: subtasks, - alarms: alarms, - location: location, url: url, ); } diff --git a/lib/model/location_alarm.dart b/lib/model/location_alarm.dart index bb1b924..0d8d10a 100644 --- a/lib/model/location_alarm.dart +++ b/lib/model/location_alarm.dart @@ -5,12 +5,16 @@ class LocationAlarm implements Alarm { @override final String id; + @override + final String taskId; + final Location location; final int radiusMeters; const LocationAlarm({ required this.id, + required this.taskId, required this.location, required this.radiusMeters, }); @@ -18,6 +22,7 @@ class LocationAlarm implements Alarm { factory LocationAlarm.fromJson(Map json) { return LocationAlarm( id: json['id'] as String, + taskId: json['taskId'] as String, location: Location.fromJson(json['location'] as Map), radiusMeters: json['radiusMeters'] as int, ); @@ -27,6 +32,7 @@ class LocationAlarm implements Alarm { Map toJson() { return { 'id': id, + 'taskId': taskId, 'location': location.toJson(), 'radiusMeters': radiusMeters, }; diff --git a/lib/model/repositories/interfaces/location_repository.dart b/lib/model/repositories/interfaces/location_repository.dart new file mode 100644 index 0000000..04db6b1 --- /dev/null +++ b/lib/model/repositories/interfaces/location_repository.dart @@ -0,0 +1,19 @@ +import '../../location.dart'; + +abstract class LocationRepository { + // Create + + Future createLocation(Location location); + + // Read + + Future> loadLocations(); + + // Update + + Future updateLocation(Location location); + + // Delete + + Future deleteLocation(Location location); +} diff --git a/lib/model/repositories/local_repository.dart b/lib/model/repositories/local_repository.dart index a9ce4a0..de6ab1a 100644 --- a/lib/model/repositories/local_repository.dart +++ b/lib/model/repositories/local_repository.dart @@ -3,14 +3,18 @@ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import '../alarm.dart'; +import '../location.dart'; import '../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: {_tasksKey, _taskOrderKey}, + allowList: { + _tasksKey, + _taskOrderKey, + _alarmsKey, + _locationsKey, + }, ), ).then((value) => _prefs = value); } @@ -29,6 +38,13 @@ class LocalRepository implements TaskRepository, AlarmRepository { _prefs!.setStringList(_tasksKey, jsonList); } + Future _saveLocations(List locations) async { + final jsonList = locations + .map((e) => jsonEncode(e.toJson())) + .toList(); + _prefs!.setStringList(_locationsKey, jsonList); + } + Future _saveTaskOrder(List 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 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((e) => Alarm.fromJson(jsonDecode(e))).toList(); } + @override + Future> loadLocations() async { + final Iterable jsonList = + _prefs!.getStringList(_locationsKey) ?? []; + return jsonList + .map((e) => Location.fromJson(jsonDecode(e))) + .toList(); + } + // Update @override @@ -106,6 +138,14 @@ class LocalRepository implements TaskRepository, AlarmRepository { _saveAlarms(alarms); } + @override + Future 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 deleteLocation(Location location) async { + final locations = await loadLocations(); + locations.remove(location); + _saveLocations(locations); + } } diff --git a/lib/model/task.dart b/lib/model/task.dart index f4554f9..cecddd7 100644 --- a/lib/model/task.dart +++ b/lib/model/task.dart @@ -9,8 +9,6 @@ class Task { final bool isCompleted; final String category; final List subtasks; - final List alarms; - final Location? location; final String url; Task({ @@ -22,8 +20,6 @@ class Task { this.isCompleted = false, this.category = '', this.subtasks = const [], - this.alarms = const [], - this.location, this.url = '', }); @@ -49,8 +45,6 @@ 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, ); } @@ -71,14 +65,6 @@ class Task { ?.map((e) => Task.fromJson(e as Map)) .toList() ?? [], - alarms: - (json['alarms'] as List?) - ?.map((e) => DateTime.parse(e as String)) - .toList() ?? - [], - location: json['location'] != null - ? Location.fromJson(json['location'] as Map) - : null, url: json['url'] as String? ?? '', ); } @@ -93,8 +79,6 @@ 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, }; } diff --git a/lib/model/time_alarm.dart b/lib/model/time_alarm.dart index 04a8f5e..f7aabc5 100644 --- a/lib/model/time_alarm.dart +++ b/lib/model/time_alarm.dart @@ -4,19 +4,31 @@ class TimeAlarm implements Alarm { @override final String id; + @override + final String taskId; + final DateTime triggerAt; - const TimeAlarm({required this.id, required this.triggerAt}); + const TimeAlarm({ + required this.id, + required this.taskId, + required this.triggerAt, + }); factory TimeAlarm.fromJson(Map json) { return TimeAlarm( id: json['id'] as String, + taskId: json['taskId'] as String, triggerAt: DateTime.parse(json['triggerAt'] as String), ); } @override Map toJson() { - return {'id': id, 'triggerAt': triggerAt.toIso8601String()}; + return { + 'id': id, + 'taskId': taskId, + 'triggerAt': triggerAt.toIso8601String(), + }; } } diff --git a/lib/pages/task_edit_page.dart b/lib/pages/task_edit_page.dart index e31fade..36c2d1d 100644 --- a/lib/pages/task_edit_page.dart +++ b/lib/pages/task_edit_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../model/callback_models/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/time_selector.dart'; @@ -139,8 +139,6 @@ class _TaskEditPageState extends State { isCompleted: false, category: categoryController.text, subtasks: [], - alarms: [], - location: null, url: urlController.text, ), ); diff --git a/lib/pages/task_overview_page.dart b/lib/pages/task_overview_page.dart index 92be7a3..9f26226 100644 --- a/lib/pages/task_overview_page.dart +++ b/lib/pages/task_overview_page.dart @@ -3,8 +3,9 @@ import 'package:flutter/material.dart'; import '../model/callback_models/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 'task_edit_page.dart'; class TaskOverviewPage extends StatefulWidget { @@ -37,24 +38,28 @@ class _TaskOverviewPageState extends State { Widget itemBuilder(BuildContext context, int index) { final task = tasks.elementAt(index); - return ListTile( + return TaskDismissible( 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() - .saveTask(task.copyWith(isCompleted: isCompleted)), + onDismissedRight: () => + context.controller().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() + .saveTask(task.copyWith(isCompleted: isCompleted)), + ), + onTap: () async { + final result = await onTaskTapped(task); + if (result != null && context.mounted) { + context.controller().saveTask( + result.toTask(id: task.id), + ); + } + }, ), - onTap: () async { - final result = await onTaskTapped(task); - if (result != null && context.mounted) { - context.controller().saveTask( - result.toTask(id: task.id), - ); - } - }, ); } diff --git a/lib/service/controllers/alarm_controller.dart b/lib/service/controllers/alarm_controller.dart new file mode 100644 index 0000000..985ae25 --- /dev/null +++ b/lib/service/controllers/alarm_controller.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../../model/alarm.dart'; +import '../../model/repositories/interfaces/alarm_repository.dart'; + +class AlarmController extends ChangeNotifier { + AlarmController(AlarmRepository repository) : _repository = repository { + _loadAlarms(); + } + + final AlarmRepository _repository; + + final List _alarms = []; + + Future addAlarm(Alarm alarm) { + _alarms.add(alarm); + notifyListeners(); + return _repository.createAlarm(alarm); + } + + Future deleteAlarm(Alarm alarm) { + _alarms.remove(alarm); + notifyListeners(); + return _repository.deleteAlarm(alarm); + } + + Future _loadAlarms() { + _alarms.clear(); + return _repository.loadAlarms().then((value) => _alarms.addAll(value)); + } +} diff --git a/lib/service/controllers/location_controller.dart b/lib/service/controllers/location_controller.dart new file mode 100644 index 0000000..ad8850a --- /dev/null +++ b/lib/service/controllers/location_controller.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../../model/location.dart'; +import '../../model/repositories/interfaces/location_repository.dart'; + +class LocationController extends ChangeNotifier { + LocationController(LocationRepository repository) : _repository = repository { + _loadLocations(); + } + + final LocationRepository _repository; + + final List _locations = []; + + Future addLocation(Location location) { + _locations.add(location); + notifyListeners(); + return _repository.createLocation(location); + } + + Future deleteLocation(Location location) { + _locations.remove(location); + notifyListeners(); + return _repository.deleteLocation(location); + } + + Future _loadLocations() { + _locations.clear(); + return _repository.loadLocations().then( + (value) => _locations.addAll(value), + ); + } +} diff --git a/lib/service/task_controller.dart b/lib/service/controllers/task_controller.dart similarity index 94% rename from lib/service/task_controller.dart rename to lib/service/controllers/task_controller.dart index 00675ea..2e9bba6 100644 --- a/lib/service/task_controller.dart +++ b/lib/service/controllers/task_controller.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart' show ChangeNotifier; -import '../model/repositories/interfaces/task_repository.dart'; -import '../model/task.dart'; +import '../../model/repositories/interfaces/task_repository.dart'; +import '../../model/task.dart'; class TaskController extends ChangeNotifier { TaskController(TaskRepository repository) : _repository = repository { diff --git a/lib/widgets/task_dismissible.dart b/lib/widgets/task_dismissible.dart new file mode 100644 index 0000000..5df9a80 --- /dev/null +++ b/lib/widgets/task_dismissible.dart @@ -0,0 +1,41 @@ +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( + 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!(); + } + } +}