From 6f60465c200983a6cf271f0f290a134de7d42270 Mon Sep 17 00:00:00 2001 From: Matthias Nehlsen Date: Mon, 18 Nov 2024 01:06:45 +0100 Subject: [PATCH] feat: checklist items --- .../tasks/state/checklist_controller.dart | 36 +++++- .../tasks/state/checklist_controller.g.dart | 2 +- .../state/checklist_item_controller.dart | 25 ++++ .../state/checklist_item_controller.g.dart | 2 +- .../tasks/ui/checkbox_item_widget.dart | 6 +- .../tasks/ui/checkbox_item_wrapper.dart | 3 +- lib/features/tasks/ui/checklist_widget.dart | 12 +- lib/features/tasks/ui/checklists_widget.dart | 2 +- lib/features/tasks/ui/task_form.dart | 3 +- lib/logic/persistence_logic.dart | 112 +++++++++++++++++- lib/widgetbook.dart | 2 + 11 files changed, 186 insertions(+), 19 deletions(-) diff --git a/lib/features/tasks/state/checklist_controller.dart b/lib/features/tasks/state/checklist_controller.dart index b045d5b85..809cef0ce 100644 --- a/lib/features/tasks/state/checklist_controller.dart +++ b/lib/features/tasks/state/checklist_controller.dart @@ -62,9 +62,43 @@ class ChecklistController extends _$ChecklistController { ); await _persistenceLogic.updateChecklist( checklistId: entryId, - title: title ?? '', + data: updated.data, ); state = AsyncData(updated); } } + + Future createChecklistItem(String? title) async { + final current = state.value; + final data = current?.data; + if (current != null && data != null && title != null) { + final created = await _persistenceLogic.createChecklistItem( + title: title, + checklist: current, + ); + + if (created != null) { + final updated = current.copyWith( + data: current.data.copyWith( + linkedChecklistItems: [ + ...data.linkedChecklistItems, + created.id, + ], + ), + ); + + await _persistenceLogic.updateChecklist( + checklistId: current.id, + data: updated.data.copyWith( + linkedChecklistItems: [ + ...data.linkedChecklistItems, + created.id, + ], + ), + ); + + state = AsyncData(updated); + } + } + } } diff --git a/lib/features/tasks/state/checklist_controller.g.dart b/lib/features/tasks/state/checklist_controller.g.dart index d7cb2d1da..1c55053e1 100644 --- a/lib/features/tasks/state/checklist_controller.g.dart +++ b/lib/features/tasks/state/checklist_controller.g.dart @@ -7,7 +7,7 @@ part of 'checklist_controller.dart'; // ************************************************************************** String _$checklistControllerHash() => - r'8ac67a972c1983b45b76908cbc1678c265234f0b'; + r'baec5f2ff72d531e038bf7e1b746806b4691f83a'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/features/tasks/state/checklist_item_controller.dart b/lib/features/tasks/state/checklist_item_controller.dart index 4864a44d6..67cf1c1b3 100644 --- a/lib/features/tasks/state/checklist_item_controller.dart +++ b/lib/features/tasks/state/checklist_item_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/cupertino.dart'; import 'package:lotti/classes/journal_entities.dart'; import 'package:lotti/database/database.dart'; import 'package:lotti/get_it.dart'; @@ -61,6 +62,30 @@ class ChecklistItemController extends _$ChecklistItemController { isChecked: !data.isChecked, ), ); + + getIt().updateChecklistItem( + checklistItemId: entryId, + data: updated.data, + ); + + state = AsyncData(updated); + } + } + + void updateTitle(String? title) { + debugPrint('updateTitle $title'); + final current = state.value; + final data = current?.data; + if (current != null && data != null && title != null) { + final updated = current.copyWith( + data: data.copyWith(title: title), + ); + + getIt().updateChecklistItem( + checklistItemId: entryId, + data: updated.data, + ); + state = AsyncData(updated); } } diff --git a/lib/features/tasks/state/checklist_item_controller.g.dart b/lib/features/tasks/state/checklist_item_controller.g.dart index 654daad5d..7c0540279 100644 --- a/lib/features/tasks/state/checklist_item_controller.g.dart +++ b/lib/features/tasks/state/checklist_item_controller.g.dart @@ -7,7 +7,7 @@ part of 'checklist_item_controller.dart'; // ************************************************************************** String _$checklistItemControllerHash() => - r'e76593b5c1751780f3bf94251b32e585083c4af2'; + r'8d81f430ebf816dd6fcc5d4be2fc111216f268d5'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/features/tasks/ui/checkbox_item_widget.dart b/lib/features/tasks/ui/checkbox_item_widget.dart index 8c565bd86..50bf997aa 100644 --- a/lib/features/tasks/ui/checkbox_item_widget.dart +++ b/lib/features/tasks/ui/checkbox_item_widget.dart @@ -10,6 +10,7 @@ class CheckboxItemWidget extends StatefulWidget { required this.title, required this.isChecked, required this.onChanged, + this.onTitleChange, this.onEdit, super.key, }); @@ -18,6 +19,7 @@ class CheckboxItemWidget extends StatefulWidget { final bool isChecked; final BoolCallback onChanged; final VoidCallback? onEdit; + final StringCallback? onTitleChange; @override State createState() => _CheckboxItemWidgetState(); @@ -38,7 +40,6 @@ class _CheckboxItemWidgetState extends State { return CheckboxListTile( title: GestureDetector( onTap: () { - debugPrint('Tapped'); setState(() { _isEditing = true; }); @@ -48,10 +49,10 @@ class _CheckboxItemWidgetState extends State { firstChild: TitleTextField( initialValue: widget.title, onSave: (title) { - debugPrint('Saved: $title'); setState(() { _isEditing = false; }); + widget.onTitleChange?.call(title); }, resetToInitialValue: true, onClear: () { @@ -80,7 +81,6 @@ class _CheckboxItemWidgetState extends State { ) : null, onChanged: (bool? value) { - debugPrint('Checked $value'); setState(() { _isChecked = value ?? false; }); diff --git a/lib/features/tasks/ui/checkbox_item_wrapper.dart b/lib/features/tasks/ui/checkbox_item_wrapper.dart index 07996beaa..57aedf431 100644 --- a/lib/features/tasks/ui/checkbox_item_wrapper.dart +++ b/lib/features/tasks/ui/checkbox_item_wrapper.dart @@ -28,7 +28,8 @@ class CheckboxItemWrapper extends ConsumerWidget { return CheckboxItemWidget( title: item.data.title, isChecked: item.data.isChecked, - onChanged: (checked) => ref.read(provider.notifier).toggleChecked(), + onChanged: (_) => ref.read(provider.notifier).toggleChecked(), + onTitleChange: ref.read(provider.notifier).updateTitle, ); }, error: ErrorWidget.new, diff --git a/lib/features/tasks/ui/checklist_widget.dart b/lib/features/tasks/ui/checklist_widget.dart index c82fe31b4..4e8a47b39 100644 --- a/lib/features/tasks/ui/checklist_widget.dart +++ b/lib/features/tasks/ui/checklist_widget.dart @@ -9,13 +9,15 @@ class ChecklistWidget extends StatefulWidget { const ChecklistWidget({ required this.title, required this.itemIds, - this.onTitleSave, + required this.onTitleSave, + required this.onCreateChecklistItem, super.key, }); final String title; final List itemIds; - final StringCallback? onTitleSave; + final StringCallback onTitleSave; + final StringCallback onCreateChecklistItem; @override State createState() => _ChecklistWidgetState(); @@ -35,7 +37,7 @@ class _ChecklistWidgetState extends State { child: TitleTextField( initialValue: widget.title, onSave: (title) { - widget.onTitleSave?.call(title); + widget.onTitleSave.call(title); setState(() { _isEditing = false; }); @@ -79,8 +81,7 @@ class _ChecklistWidgetState extends State { ), child: TitleTextField( onSave: (title) { - debugPrint('Saved: $title'); - widget.onTitleSave?.call(title); + widget.onCreateChecklistItem.call(title); }, clearOnSave: true, semanticsLabel: 'Add item to checklist', @@ -114,6 +115,7 @@ class ChecklistWrapper extends ConsumerWidget { title: checklist.data.title, itemIds: checklist.data.linkedChecklistItems, onTitleSave: notifier.updateTitle, + onCreateChecklistItem: notifier.createChecklistItem, ); } } diff --git a/lib/features/tasks/ui/checklists_widget.dart b/lib/features/tasks/ui/checklists_widget.dart index 522a309fc..920ae8dc0 100644 --- a/lib/features/tasks/ui/checklists_widget.dart +++ b/lib/features/tasks/ui/checklists_widget.dart @@ -9,8 +9,8 @@ import 'package:lotti/logic/create/create_entry.dart'; class ChecklistsWidget extends ConsumerWidget { const ChecklistsWidget({ required this.entryId, - super.key, required this.task, + super.key, }); final String entryId; diff --git a/lib/features/tasks/ui/task_form.dart b/lib/features/tasks/ui/task_form.dart index debffb1a9..f7beb0960 100644 --- a/lib/features/tasks/ui/task_form.dart +++ b/lib/features/tasks/ui/task_form.dart @@ -7,13 +7,12 @@ import 'package:lotti/classes/journal_entities.dart'; import 'package:lotti/features/journal/state/entry_controller.dart'; import 'package:lotti/features/journal/ui/widgets/editor/editor_widget.dart'; import 'package:lotti/features/journal/util/entry_tools.dart'; +import 'package:lotti/features/tasks/ui/checklists_widget.dart'; import 'package:lotti/l10n/app_localizations_context.dart'; import 'package:lotti/themes/theme.dart'; import 'package:lotti/widgets/categories/category_field.dart'; import 'package:lotti/widgets/date_time/duration_bottom_sheet.dart'; -import 'checklists_widget.dart'; - class TaskForm extends ConsumerStatefulWidget { const TaskForm( this.task, { diff --git a/lib/logic/persistence_logic.dart b/lib/logic/persistence_logic.dart index c09690ec2..b24fa7d7e 100644 --- a/lib/logic/persistence_logic.dart +++ b/lib/logic/persistence_logic.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:lotti/classes/audio_note.dart'; import 'package:lotti/classes/checklist_data.dart'; +import 'package:lotti/classes/checklist_item_data.dart'; import 'package:lotti/classes/entity_definitions.dart'; import 'package:lotti/classes/entry_links.dart'; import 'package:lotti/classes/entry_text.dart'; @@ -576,9 +577,64 @@ class PersistenceLogic { } } + Future createChecklistItem({ + required Checklist checklist, + required String title, + }) async { + try { + final now = DateTime.now(); + final id = uuid.v1(); + final vc = await _vectorClockService.getNextVectorClock(); + + final newChecklistItem = JournalEntity.checklistItem( + meta: Metadata( + createdAt: now, + updatedAt: now, + dateFrom: now, + dateTo: now, + id: id, + vectorClock: vc, + timezone: await getLocalTimezone(), + utcOffset: now.timeZoneOffset.inMinutes, + ), + data: ChecklistItemData( + title: title, + isChecked: false, + linkedChecklists: [], + ), + ); + + await createDbEntity( + newChecklistItem, + enqueueSync: true, + ); + addGeolocation(id); + + await updateChecklist( + checklistId: checklist.id, + data: checklist.data.copyWith( + linkedChecklistItems: [ + ...checklist.data.linkedChecklistItems, + newChecklistItem.meta.id, + ], + ), + ); + + return newChecklistItem; + } catch (exception, stackTrace) { + _loggingDb.captureException( + exception, + domain: 'persistence_logic', + subDomain: 'createChecklistEntry', + stackTrace: stackTrace, + ); + return null; + } + } + Future updateChecklist({ required String checklistId, - required String title, + required ChecklistData data, }) async { try { final now = DateTime.now(); @@ -600,12 +656,12 @@ class PersistenceLogic { vectorClock: vc, ); - final newTask = checklist.copyWith( + final updatedChecklist = checklist.copyWith( meta: newMeta, - data: checklist.data.copyWith(title: title), + data: data, ); - await updateDbEntity(newTask, enqueueSync: true); + await updateDbEntity(updatedChecklist, enqueueSync: true); }, orElse: () async => _loggingDb.captureException( 'not a checklist', @@ -624,6 +680,54 @@ class PersistenceLogic { return true; } + Future updateChecklistItem({ + required String checklistItemId, + required ChecklistItemData data, + }) async { + try { + final now = DateTime.now(); + final journalEntity = await _journalDb.journalEntityById(checklistItemId); + + if (journalEntity == null) { + return false; + } + + await journalEntity.maybeMap( + checklistItem: (ChecklistItem checklistItem) async { + final vc = await _vectorClockService.getNextVectorClock( + previous: journalEntity.meta.vectorClock, + ); + + final oldMeta = journalEntity.meta; + final newMeta = oldMeta.copyWith( + updatedAt: now, + vectorClock: vc, + ); + + final updatedChecklist = checklistItem.copyWith( + meta: newMeta, + data: data, + ); + + await updateDbEntity(updatedChecklist, enqueueSync: true); + }, + orElse: () async => _loggingDb.captureException( + 'not a checklist item', + domain: 'persistence_logic', + subDomain: 'updateChecklistItem', + ), + ); + } catch (exception, stackTrace) { + _loggingDb.captureException( + exception, + domain: 'persistence_logic', + subDomain: 'updateChecklistItem', + stackTrace: stackTrace, + ); + } + return true; + } + Future createLink({ required String fromId, required String toId, diff --git a/lib/widgetbook.dart b/lib/widgetbook.dart index c5eba0d8e..d6562b5a8 100644 --- a/lib/widgetbook.dart +++ b/lib/widgetbook.dart @@ -125,6 +125,8 @@ class WidgetbookApp extends StatelessWidget { checklistItem2.meta.id, checklistItem3.meta.id, ], + onCreateChecklistItem: (title) {}, + onTitleSave: (title) {}, ), ), ),