parsing of todo, retaining state over multiple sessions.
This commit is contained in:
parent
60edaddc03
commit
61d444bd41
2
.gitignore
vendored
2
.gitignore
vendored
@ -41,4 +41,4 @@ app.*.map.json
|
|||||||
# Android Studio will place build artifacts here
|
# Android Studio will place build artifacts here
|
||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:nextcloud_reminder/repeating_task.dart';
|
import 'package:nextcloud_reminder/repeating_task.dart';
|
||||||
|
import 'package:nextcloud_reminder/types/tasks.dart';
|
||||||
|
|
||||||
class AddTaskWidget extends StatefulWidget {
|
class AddTaskWidget extends StatefulWidget {
|
||||||
const AddTaskWidget({super.key, this.restorationId, required this.onSave});
|
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.')),
|
const SnackBar(content: Text('Task added.')),
|
||||||
);
|
);
|
||||||
widget.onSave(RepeatingTask(
|
widget.onSave(RepeatingTask(
|
||||||
title: _titleController.text, begin: _beginDate.value, repeat: _repeat,));
|
task: Task(title: _titleController.text, begin: _beginDate.value), repeat: _repeat,));
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:nextcloud_reminder/addReminder.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/repeating_task.dart';
|
||||||
import 'package:nextcloud_reminder/table.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 {
|
class HomeWidget extends StatefulWidget {
|
||||||
const HomeWidget({super.key, required this.title});
|
const HomeWidget({super.key, required this.title});
|
||||||
@ -21,9 +26,60 @@ class HomeWidget extends StatefulWidget {
|
|||||||
State<HomeWidget> createState() => _HomeWidgetState();
|
State<HomeWidget> createState() => _HomeWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeWidgetState extends State<HomeWidget> {
|
class _HomeWidgetState extends State<HomeWidget> with WidgetsBindingObserver {
|
||||||
|
|
||||||
final ScrollTable _checkboxes = ScrollTable(title: "TODOs");
|
final ScrollTable _checkboxes = ScrollTable(title: "TODOs");
|
||||||
int _counter = 1;
|
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() {
|
void _addDummyTask() {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -32,13 +88,14 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||||||
// so that the display can reflect the updated values. If we changed
|
// so that the display can reflect the updated values. If we changed
|
||||||
// stuff without calling setState(), then the build method would not be
|
// stuff without calling setState(), then the build method would not be
|
||||||
// called again, and so nothing would appear to happen.
|
// 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) {
|
void _addTask(RepeatingTask? t) {
|
||||||
if (t != null) {
|
if (t != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_checkboxes.addTask(t!);
|
_checkboxes.addTask(t!);
|
||||||
|
tasks.add(t.task);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
79
lib/parser/todotxt.dart
Normal file
79
lib/parser/todotxt.dart
Normal 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])));
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:nextcloud_reminder/task_item.dart';
|
import 'package:nextcloud_reminder/task_item.dart';
|
||||||
|
import 'package:nextcloud_reminder/types/tasks.dart';
|
||||||
|
|
||||||
class RepeatingTask extends StatefulWidget {
|
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 Task task;
|
||||||
final DateTime begin;
|
|
||||||
final int repeat;
|
final int repeat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -14,14 +14,10 @@ class RepeatingTask extends StatefulWidget {
|
|||||||
|
|
||||||
class _RepeatingTaskState extends State<RepeatingTask> {
|
class _RepeatingTaskState extends State<RepeatingTask> {
|
||||||
late List<TaskItem> _occurrences;
|
late List<TaskItem> _occurrences;
|
||||||
late DateTime _first_occurence;
|
|
||||||
late String _title;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_title = widget.title;
|
|
||||||
_first_occurence = widget.begin;
|
|
||||||
_occurrences = List<TaskItem>.generate(10, (index) => TaskItem(done: index % widget.repeat == 0 ? false : null));
|
_occurrences = List<TaskItem>.generate(10, (index) => TaskItem(done: index % widget.repeat == 0 ? false : null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class _ScrollTableState extends State<ScrollTable> {
|
|||||||
height: 60.0,
|
height: 60.0,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
margin: const EdgeInsets.all(4.0),
|
margin: const EdgeInsets.all(4.0),
|
||||||
child: Text(t.title, style: Theme
|
child: Text(t.task.title, style: Theme
|
||||||
.of(context)
|
.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.labelMedium),
|
.labelMedium),
|
||||||
|
32
lib/types/tasks.dart
Normal file
32
lib/types/tasks.dart
Normal 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(" ")
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,8 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import path_provider_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
}
|
}
|
||||||
|
113
pubspec.lock
113
pubspec.lock
@ -50,6 +50,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.4"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -102,6 +116,83 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.2"
|
version: "1.8.2"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.11"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.22"
|
||||||
|
path_provider_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_ios
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.11"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.7"
|
||||||
|
path_provider_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_macos
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.6"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.5"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
petitparser:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.0"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
process:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: process
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.4"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -149,6 +240,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.12"
|
version: "0.4.12"
|
||||||
|
tuple:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: tuple
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -156,5 +254,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0+2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.18.6 <3.0.0"
|
dart: ">=2.18.6 <3.0.0"
|
||||||
|
flutter: ">=3.0.0"
|
||||||
|
@ -36,6 +36,9 @@ dependencies:
|
|||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
|
path_provider: ^2.0.11
|
||||||
|
petitparser: ^5.1.0
|
||||||
|
tuple: ^2.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user