diff --git a/README.md b/README.md index ed45bca..5a9b7ed 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ A Reminder based on todo.txt synced via nextcloud ## Current todos: -- [ ] make repeat-datatype (like: daily, weekly on mo/th/fr, bi-monthly, etc.) - - [ ] define isomorphism for 'repeat:'-meta-tag +- [x] make repeat-datatype (like: daily, weekly on mo/th/fr, bi-monthly, etc.) + - [x] define isomorphism for 'repeat:'-meta-tag - [ ] add interface for repeat-datatype in addReminder.dart - [x] save/load data to/from disk - [x] adding/removing tasks -- [ ] respect ordering that was used when starting the app when saving. +- [x] respect ordering that was used when starting the app when saving. - [ ] add Nextcloud-login for getting a Token - [ ] use webdav for synchronizing with Nextcloud using that token - [ ] sorting by "next up", "priority" @@ -28,6 +28,10 @@ A Reminder based on todo.txt synced via nextcloud ### Adding Tasks ![](img/2023-01-08_addTask.png) +(still missing repeat-options, currently defaults to daily.) ### Details/Removing tasks -![](img/2023-01-10_Task_details.png) \ No newline at end of file +![](img/2023-01-10_Task_details.png) + +### Complex repeat patterns +![](img/2023-01-10_repeat_patterns.png) \ No newline at end of file diff --git a/img/2023-01-10_repeat_patterns.png b/img/2023-01-10_repeat_patterns.png new file mode 100644 index 0000000..e54d7c3 Binary files /dev/null and b/img/2023-01-10_repeat_patterns.png differ diff --git a/lib/addReminder.dart b/lib/addReminder.dart index e873043..4adee6a 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/repeat.dart'; import 'package:nextcloud_reminder/types/tasks.dart'; class AddTaskWidget extends StatefulWidget { @@ -124,7 +125,7 @@ class _AddTaskWidgetState extends State with RestorationMixin { const SnackBar(content: Text('Task added.')), ); widget.onSave(RepeatingTask( - task: Task(title: _titleController.text, begin: _beginDate.value), repeat: _repeat,)); + task: TaskExtra(title: _titleController.text, begin: _beginDate.value, repeat: [RepeatInterval(interval: DateInterval.daily)],))); Navigator.pop(context); } }, diff --git a/lib/homescreen.dart b/lib/homescreen.dart index e4d81af..2f3926f 100644 --- a/lib/homescreen.dart +++ b/lib/homescreen.dart @@ -8,6 +8,7 @@ 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'; +import 'package:tuple/tuple.dart'; class HomeWidget extends StatefulWidget { const HomeWidget({super.key, required this.title}); @@ -30,10 +31,9 @@ class HomeWidget extends StatefulWidget { class _HomeWidgetState extends State with WidgetsBindingObserver { late final ScrollTable _checkboxes; - int _counter = 1; late final Directory appDocDirectory; late final File todotxt = File('${appDocDirectory.path}/todo.txt'); - final List tasks = []; + final List tasks = []; void _loadTodos() { for (var element in tasks) { @@ -81,7 +81,9 @@ class _HomeWidgetState extends State with WidgetsBindingObserver { } void saveData() async { //TODO: better update lines instead of blindly overwriting. - String data = tasks.map((t) => t.toString()).join("\n"); + List tmp = tasks; + tmp.sort((a, b) => a.lineNumber != null && b.lineNumber != null ? a.lineNumber! - b.lineNumber! : -1,); + String data = tmp.map((t) => t.formatAsTask()).join("\n"); debugPrint("Saving:\n$data"); await todotxt.writeAsString(data); } @@ -90,7 +92,7 @@ class _HomeWidgetState extends State with WidgetsBindingObserver { if (t != null) { setState(() { _checkboxes.addTask(t!); - tasks.add(t.task); + tasks.add(TaskExtra.fromTask(t.task)); }); } } diff --git a/lib/parser/todotxt.dart b/lib/parser/todotxt.dart index caf59fe..2534797 100644 --- a/lib/parser/todotxt.dart +++ b/lib/parser/todotxt.dart @@ -9,16 +9,18 @@ abstract class TodoParser { static final _todoParser = _definition.build(); - static List parse(List input) { - final List ret = []; + static List parse(List input) { + final List ret = []; + var line=1; for (var element in input) { var parsed = _todoParser.parse(element); if (parsed.isSuccess) { - ret.add(parsed.value); + ret.add(TaskExtra.fromTask(parsed.value, lineNumber: line)); } else { debugPrint(parsed.message); debugPrint(element); } + line++; } return ret; } diff --git a/lib/repeating_task.dart b/lib/repeating_task.dart index ddb2070..cd6776a 100644 --- a/lib/repeating_task.dart +++ b/lib/repeating_task.dart @@ -3,10 +3,9 @@ import 'package:nextcloud_reminder/task_item.dart'; import 'package:nextcloud_reminder/types/tasks.dart'; class RepeatingTask extends StatefulWidget { - const RepeatingTask({super.key, required this.task, this.repeat=1}); + const RepeatingTask({super.key, required this.task}); - final Task task; - final int repeat; + final TaskExtra task; @override State createState() => _RepeatingTaskState(); @@ -18,7 +17,17 @@ class _RepeatingTaskState extends State { @override void initState() { super.initState(); - _occurrences = List.generate(10, (index) => TaskItem(done: index % widget.repeat == 0 ? false : null)); + _occurrences = List.generate(10, (index) { + var start = widget.task.begin ?? DateTime.now(); + var comparator = DateTime.now().add(Duration(days: index)); + for (var r in widget.task.repeat) { + if (r.repeatHit(start, comparator)) return TaskItem(done: widget.task.done,); + } + if (widget.task.repeat.isEmpty && start.day == comparator.day && start.month == comparator.month && start.year == comparator.year) { + return TaskItem(done: widget.task.done,); + } + return const TaskItem(done: null); + }); } @override diff --git a/lib/types/repeat.dart b/lib/types/repeat.dart new file mode 100644 index 0000000..bebacb2 --- /dev/null +++ b/lib/types/repeat.dart @@ -0,0 +1,97 @@ +class RepeatInterval { + int every; + DateInterval interval; + + RepeatInterval({required this.interval, this.every=1}); + + static RepeatInterval? fromString(String input) { + RegExpMatch? m = RegExp(r'(\d*)(\D+)').firstMatch(input); + if (m != null) { + DateInterval? di = _stringToInterval(m!.group(2)!); + if (di != null) { + return RepeatInterval(interval: di, every: m?.group(1) == "" ? 1 : int.parse(m!.group(1)!)); + } + } + return null; + } + + /// Does the RepeatInterval referencing from a hit b? + bool repeatHit(DateTime a, DateTime b) { + var daydiff = a.difference(b).inDays; + var weeks = (daydiff / 7).floor(); + switch (interval) { + case DateInterval.daily: return daydiff % every == 0; + case DateInterval.weekly: return daydiff % (every*7) == 0; + case DateInterval.monthly: return a.day == b.day; + case DateInterval.monday: return weeks % every == 0 && b.weekday == DateTime.monday; + case DateInterval.tuesday: return weeks % every == 0 && b.weekday == DateTime.tuesday; + case DateInterval.wednesday: return weeks % every == 0 && b.weekday == DateTime.wednesday; + case DateInterval.thursday: return weeks % every == 0 && b.weekday == DateTime.thursday; + case DateInterval.friday: return weeks % every == 0 && b.weekday == DateTime.friday; + case DateInterval.saturday: return weeks % every == 0 && b.weekday == DateTime.saturday; + case DateInterval.sunday: return weeks % every == 0 && b.weekday == DateTime.sunday; + case DateInterval.dayOfMonth: return b.day == every; + case DateInterval.dayOfYear: return DateTime.now().difference(b).inDays == every; + } + } + + @override + toString() { + return "$every${_intervalToString(interval)}"; + } + + static DateInterval? _stringToInterval(String input) { + switch (input) { + case "daily": + case "day": return DateInterval.daily; + case "weekly": + case "week": return DateInterval.weekly; + case "monthly": + case "month": return DateInterval.monthly; + case "mon": return DateInterval.monday; + case "tue": return DateInterval.tuesday; + case "wed": return DateInterval.wednesday; + case "thu": return DateInterval.thursday; + case "fri": return DateInterval.friday; + case "sat": return DateInterval.saturday; + case "sun": return DateInterval.sunday; + case "ofMonth": return DateInterval.dayOfMonth; + case "ofYear": return DateInterval.dayOfYear; + } + return null; + } + + static String _intervalToString(DateInterval di) { + switch (di) { + case DateInterval.daily: return "day"; + case DateInterval.weekly: return "week"; + case DateInterval.monthly: return "month"; + case DateInterval.monday: return "mon"; + case DateInterval.tuesday: return "tue"; + case DateInterval.wednesday: return "wed"; + case DateInterval.thursday: return "thu"; + case DateInterval.friday: return "fri"; + case DateInterval.saturday: return "sat"; + case DateInterval.sunday: return "sun"; + case DateInterval.dayOfMonth: return "ofMonth"; + case DateInterval.dayOfYear: return "ofYear"; + } + } + +} + +enum DateInterval { + daily, + weekly, + monthly, + monday, + tuesday, + wednesday, + thursday, + friday, + saturday, + sunday, + dayOfMonth, + dayOfYear, +} + diff --git a/lib/types/tasks.dart b/lib/types/tasks.dart index f4fa9d6..376e8f8 100644 --- a/lib/types/tasks.dart +++ b/lib/types/tasks.dart @@ -1,3 +1,45 @@ + +import 'package:collection/collection.dart'; +import 'package:nextcloud_reminder/types/repeat.dart'; + +class TaskExtra extends Task { + TaskExtra({required super.title, + super.done = false, + super.contexts = const [], + super.projects = const [], + super.meta = const {}, + super.priority, + super.begin, + super.end, + this.lineNumber, + this.repeat = const [], + }); + + TaskExtra.fromTask(Task t, {this.lineNumber}) + : repeat = t.meta["repeat"]?.split("/").map(RepeatInterval.fromString).whereNotNull().toList() ?? [] + , super(title: t.title, + done: t.done, + contexts: t.contexts, + projects: t.projects, + meta: t.meta, + priority: t.priority, + begin: t.begin, + end: t.end, + ); + + final int? lineNumber; + final List repeat; + + formatAsTask() { + return super.toString(); + } + + @override + String toString() { + return "$lineNumber: ${super.toString()}"; + } +} + class Task { final bool done; final DateTime? begin; diff --git a/pubspec.lock b/pubspec.lock index 82d7417..0a7bb28 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -30,7 +30,7 @@ packages: source: hosted version: "1.1.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index f97f81f..f0e3fed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: petitparser: ^5.1.0 tuple: ^2.0.1 flutter_window_close: ^0.2.2 + collection: ^1.16.0 dev_dependencies: flutter_test: