parsing of todo, retaining state over multiple sessions.

This commit is contained in:
2023-01-09 17:27:04 +01:00
parent 60edaddc03
commit 61d444bd41
10 changed files with 295 additions and 12 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:nextcloud_reminder/repeating_task.dart';
import 'package:nextcloud_reminder/types/tasks.dart';
class AddTaskWidget extends StatefulWidget {
const AddTaskWidget({super.key, this.restorationId, required this.onSave});
@ -123,7 +124,7 @@ class _AddTaskWidgetState extends State<AddTaskWidget> with RestorationMixin {
const SnackBar(content: Text('Task added.')),
);
widget.onSave(RepeatingTask(
title: _titleController.text, begin: _beginDate.value, repeat: _repeat,));
task: Task(title: _titleController.text, begin: _beginDate.value), repeat: _repeat,));
Navigator.pop(context);
}
},

View File

@ -1,7 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:nextcloud_reminder/addReminder.dart';
import 'package:nextcloud_reminder/parser/todotxt.dart';
import 'package:nextcloud_reminder/repeating_task.dart';
import 'package:nextcloud_reminder/table.dart';
import 'package:nextcloud_reminder/types/tasks.dart';
import 'package:path_provider/path_provider.dart';
class HomeWidget extends StatefulWidget {
const HomeWidget({super.key, required this.title});
@ -21,9 +26,60 @@ class HomeWidget extends StatefulWidget {
State<HomeWidget> createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
class _HomeWidgetState extends State<HomeWidget> with WidgetsBindingObserver {
final ScrollTable _checkboxes = ScrollTable(title: "TODOs");
int _counter = 1;
late final Directory appDocDirectory;
late final File todotxt = File('${appDocDirectory.path}/todo.txt');
final List<Task> tasks = [];
void _loadTodos() {
for (var element in tasks) {
debugPrint("loaded: ${element.toString()}");
_checkboxes.addTask(RepeatingTask(task: element));
}
}
void initLazy() async {
appDocDirectory = (await getApplicationDocumentsDirectory());
if (await todotxt.exists()) {
var data = await todotxt.readAsLines();
debugPrint(data.toString());
tasks.addAll(TodoParser.parse(data));
_loadTodos();
} else {
debugPrint("no todo file found at ${todotxt.path}");
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
initLazy();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) {
// App is in the background, save any unsaved data
debugPrint("save to disk");
saveData();
}
}
void saveData() async {
//TODO: better update lines instead of blindly overwriting.
String data = tasks.map((t) => t.toString()).join("\n");
debugPrint("Saving:\n$data");
await todotxt.writeAsString(data);
}
void _addDummyTask() {
setState(() {
@ -32,13 +88,14 @@ class _HomeWidgetState extends State<HomeWidget> {
// so that the display can reflect the updated values. If we changed
// stuff without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_checkboxes.addTask(RepeatingTask(title: "Dummy Task #$_counter", begin: DateTime.now(), repeat: _counter++));
_checkboxes.addTask(RepeatingTask(task: Task(title: "Dummy Task #$_counter", begin: DateTime.now()), repeat: _counter++));
});
}
void _addTask(RepeatingTask? t) {
if (t != null) {
setState(() {
_checkboxes.addTask(t!);
tasks.add(t.task);
});
}
}

79
lib/parser/todotxt.dart Normal file
View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:petitparser/petitparser.dart';
import 'package:nextcloud_reminder/types/tasks.dart';
import 'package:tuple/tuple.dart';
abstract class TodoParser {
static final _definition = TodoTxtEvaluatorDefinition();
static final _todoParser = _definition.build();
static List<Task> parse(List<String> input) {
final List<Task> ret = [];
for (var element in input) {
var parsed = _todoParser.parse(element);
if (parsed.isSuccess) {
ret.add(parsed.value);
} else {
debugPrint(parsed.message);
debugPrint(element);
}
}
return ret;
}
}
class TodoTxtEvaluatorDefinition extends TodoTxtDefinition {
@override
Parser todoLine() => super.todoLine().map((value) =>
//parser gives the following structure:
//0: completed? (null | true)
//1: priority? (null | uppercase letter)
//2: range? (null | [begin-date, (null | end-date)]
//3: description: Tuple5<original text, list<projects>, list<contexts>, map<meta>, text without meta>
Task( title: value[3].item5,
done: value[0] ?? false,
priority: value[1],
begin: (value[2] == null ? null : value[2][0]),
end: (value[2] == null ? null : value[2][1]),
projects: value[3].item2,
contexts: value[3].item3,
meta: value[3].item4,
)
);
}
class TodoTxtDefinition extends GrammarDefinition {
Tuple5<String,List<String>,List<String>, Map<String,String>, String> _extract_project_and_context(String input) {
final projectPattern = RegExp(r'\+([^ ]+)');
final contextPattern = RegExp(r'@([^ ]+)');
final metaPattern = RegExp(r'([^ ]+):([^ ]+)');
var projects = projectPattern.allMatches(input).map((Match m) => m[1]!);
var contexts = contextPattern.allMatches(input).map((Match m) => m[1]!);
var meta = metaPattern.allMatches(input).map((Match m) => MapEntry(m[1]!, m[2]!));
var cleanInput = input//.replaceAll(projectPattern, '')
//.replaceAll(contextPattern, '')
.replaceAll(metaPattern, '');
return Tuple5(input, List.of(projects), List.of(contexts), Map.fromEntries(meta), cleanInput.trim());
}
@override
Parser start() => ref0(todoLine).end();
Parser todoLine() => ref0(completed).trim().optional()
& ref0(priority).trim().optional()
& (ref0(date).trim() & ref0(date).trim().optional()).optional() // completion date + optional creation date
& any().plus().flatten().map(_extract_project_and_context)
;
Parser completed() => char('x').map((_) => true);
Parser priority() => (char('(') & pattern('A-Z') & char(')')).map((values) => values[1]);
Parser date() => (digit().plus().flatten() & char('-') & digit().plus().flatten() & char('-') & digit().plus().flatten())
.map((values) => DateTime(int.parse(values[0]),int.parse(values[2]),int.parse(values[4])));
}

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:nextcloud_reminder/task_item.dart';
import 'package:nextcloud_reminder/types/tasks.dart';
class RepeatingTask extends StatefulWidget {
const RepeatingTask({super.key, required this.title, required this.begin, this.repeat=1});
const RepeatingTask({super.key, required this.task, this.repeat=1});
final String title;
final DateTime begin;
final Task task;
final int repeat;
@override
@ -14,14 +14,10 @@ class RepeatingTask extends StatefulWidget {
class _RepeatingTaskState extends State<RepeatingTask> {
late List<TaskItem> _occurrences;
late DateTime _first_occurence;
late String _title;
@override
void initState() {
super.initState();
_title = widget.title;
_first_occurence = widget.begin;
_occurrences = List<TaskItem>.generate(10, (index) => TaskItem(done: index % widget.repeat == 0 ? false : null));
}

View File

@ -39,7 +39,7 @@ class _ScrollTableState extends State<ScrollTable> {
height: 60.0,
color: Colors.white,
margin: const EdgeInsets.all(4.0),
child: Text(t.title, style: Theme
child: Text(t.task.title, style: Theme
.of(context)
.textTheme
.labelMedium),

32
lib/types/tasks.dart Normal file
View File

@ -0,0 +1,32 @@
class Task {
final bool done;
final DateTime? begin;
final DateTime? end;
final String title;
final List<String> contexts;
final List<String> projects;
final Map<String,String> meta;
final String? priority;
Task({
required this.title,
this.done = false,
this.contexts = const [],
this.projects = const [],
this.meta = const {},
this.priority,
this.begin,
this.end,
});
@override
String toString() {
return (done ? "x " : "")
+ (priority == null ? "" : "(${priority!}) ")
+ (begin == null ? "" : "${begin!.year}-${begin!.month}-${begin!.day} ")
+ (end == null ? "" : "${end!.year}-${end!.month}-${end!.day} ")
+ ("$title ")
+ meta.entries.map((entry) => "${entry.key}:${entry.value}").join(" ")
;
}
}