From 61d444bd415ae34c63d1f6f334b8178167a6e592 Mon Sep 17 00:00:00 2001 From: Stefan Dresselhaus Date: Mon, 9 Jan 2023 17:27:04 +0100 Subject: [PATCH] parsing of todo, retaining state over multiple sessions. --- .gitignore | 2 +- lib/addReminder.dart | 3 +- lib/homescreen.dart | 61 +++++++++- lib/parser/todotxt.dart | 79 ++++++++++++ lib/repeating_task.dart | 10 +- lib/table.dart | 2 +- lib/types/tasks.dart | 32 +++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 113 ++++++++++++++++++ pubspec.yaml | 3 + 10 files changed, 295 insertions(+), 12 deletions(-) create mode 100644 lib/parser/todotxt.dart create mode 100644 lib/types/tasks.dart diff --git a/.gitignore b/.gitignore index 24476c5..4314849 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,4 @@ app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile -/android/app/release +/android/app/release \ No newline at end of file diff --git a/lib/addReminder.dart b/lib/addReminder.dart index 82785fc..01a0570 100644 --- a/lib/addReminder.dart +++ b/lib/addReminder.dart @@ -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 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); } }, diff --git a/lib/homescreen.dart b/lib/homescreen.dart index fd00b40..fa98a16 100644 --- a/lib/homescreen.dart +++ b/lib/homescreen.dart @@ -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 createState() => _HomeWidgetState(); } -class _HomeWidgetState extends State { +class _HomeWidgetState extends State 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 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 { // 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); }); } } diff --git a/lib/parser/todotxt.dart b/lib/parser/todotxt.dart new file mode 100644 index 0000000..caf59fe --- /dev/null +++ b/lib/parser/todotxt.dart @@ -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 parse(List input) { + final List 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, list, map, 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,List, Map, 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]))); +} \ No newline at end of file diff --git a/lib/repeating_task.dart b/lib/repeating_task.dart index cbe2e34..34161c6 100644 --- a/lib/repeating_task.dart +++ b/lib/repeating_task.dart @@ -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 { late List _occurrences; - late DateTime _first_occurence; - late String _title; @override void initState() { super.initState(); - _title = widget.title; - _first_occurence = widget.begin; _occurrences = List.generate(10, (index) => TaskItem(done: index % widget.repeat == 0 ? false : null)); } diff --git a/lib/table.dart b/lib/table.dart index 8c179ce..52bbf4f 100644 --- a/lib/table.dart +++ b/lib/table.dart @@ -39,7 +39,7 @@ class _ScrollTableState extends State { 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), diff --git a/lib/types/tasks.dart b/lib/types/tasks.dart new file mode 100644 index 0000000..b52bc8a --- /dev/null +++ b/lib/types/tasks.dart @@ -0,0 +1,32 @@ +class Task { + final bool done; + final DateTime? begin; + final DateTime? end; + final String title; + final List contexts; + final List projects; + final Map 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(" ") + ; + } +} \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..0d56f51 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import path_provider_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 0f3a736..3724a95 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,6 +50,20 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: "direct main" description: flutter @@ -102,6 +116,83 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: flutter @@ -149,6 +240,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.12" + tuple: + dependency: "direct main" + description: + name: tuple + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" vector_math: dependency: transitive description: @@ -156,5 +254,20 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dart: ">=2.18.6 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 961301f..a211f60 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,9 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + path_provider: ^2.0.11 + petitparser: ^5.1.0 + tuple: ^2.0.1 dev_dependencies: flutter_test: