Compare commits
15 Commits
v0.1.1
...
6dc7161b41
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dc7161b41 | |||
| f1756b30d1 | |||
| 77a524f3ec | |||
| 064c014f8b | |||
| 410a7eb843 | |||
| 20b017b066 | |||
| a3258b84fe | |||
| f088b84c54 | |||
| 5a39148577 | |||
| 2b718b5bd8 | |||
| 023610804d | |||
| cee5af0f84 | |||
| 999023e48a | |||
| 82a74a66b5 | |||
| e645081204 |
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
AppTheme._();
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -1,10 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_theme.dart';
|
||||
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 +16,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,6 +34,8 @@ 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(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'time_alarm.dart';
|
||||
|
||||
abstract class Alarm {
|
||||
String get id;
|
||||
String get taskId;
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
factory Alarm.fromJson(Map<String, dynamic> json) {
|
||||
@@ -25,5 +26,5 @@ abstract class Alarm {
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
int get hashCode => taskId.hashCode;
|
||||
}
|
||||
|
||||
@@ -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<Task> subtasks;
|
||||
final List<DateTime> 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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<String, dynamic> json) {
|
||||
return LocationAlarm(
|
||||
id: json['id'] as String,
|
||||
taskId: json['taskId'] as String,
|
||||
location: Location.fromJson(json['location'] as Map<String, dynamic>),
|
||||
radiusMeters: json['radiusMeters'] as int,
|
||||
);
|
||||
@@ -27,6 +32,7 @@ class LocationAlarm implements Alarm {
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'taskId': taskId,
|
||||
'location': location.toJson(),
|
||||
'radiusMeters': radiusMeters,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import '../../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);
|
||||
}
|
||||
@@ -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: <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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ class Task {
|
||||
final bool isCompleted;
|
||||
final String category;
|
||||
final List<Task> subtasks;
|
||||
final List<DateTime> 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<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
alarms:
|
||||
(json['alarms'] as List<dynamic>?)
|
||||
?.map((e) => DateTime.parse(e as String))
|
||||
.toList() ??
|
||||
[],
|
||||
location: json['location'] != null
|
||||
? Location.fromJson(json['location'] as Map<String, dynamic>)
|
||||
: 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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<String, dynamic> json) {
|
||||
return TimeAlarm(
|
||||
id: json['id'] as String,
|
||||
taskId: json['taskId'] as String,
|
||||
triggerAt: DateTime.parse(json['triggerAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'id': id, 'triggerAt': triggerAt.toIso8601String()};
|
||||
return {
|
||||
'id': id,
|
||||
'taskId': taskId,
|
||||
'triggerAt': triggerAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -80,7 +80,9 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
horizontal: MediaQuery.of(context).size.width * 0.05,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(height: 6),
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
controller: titleController,
|
||||
@@ -139,8 +141,6 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||
isCompleted: false,
|
||||
category: categoryController.text,
|
||||
subtasks: [],
|
||||
alarms: [],
|
||||
location: null,
|
||||
url: urlController.text,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
@@ -21,11 +22,16 @@ 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')),
|
||||
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 +43,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),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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,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<Location> _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> _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 '../../model/repositories/interfaces/task_repository.dart';
|
||||
import '../../model/task.dart';
|
||||
|
||||
class TaskController extends ChangeNotifier {
|
||||
TaskController(TaskRepository repository) : _repository = repository {
|
||||
@@ -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!();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user