diff --git a/lib/classes/checklist_data.dart b/lib/classes/checklist_data.dart index 52e3e25ea..bf8227a4d 100644 --- a/lib/classes/checklist_data.dart +++ b/lib/classes/checklist_data.dart @@ -8,6 +8,7 @@ class ChecklistData with _$ChecklistData { const factory ChecklistData({ required String title, required List linkedChecklistItems, + required List linkedTasks, }) = _ChecklistData; factory ChecklistData.fromJson(Map json) => diff --git a/lib/classes/checklist_data.freezed.dart b/lib/classes/checklist_data.freezed.dart index a8c750a47..dee420b5b 100644 --- a/lib/classes/checklist_data.freezed.dart +++ b/lib/classes/checklist_data.freezed.dart @@ -22,6 +22,7 @@ ChecklistData _$ChecklistDataFromJson(Map json) { mixin _$ChecklistData { String get title => throw _privateConstructorUsedError; List get linkedChecklistItems => throw _privateConstructorUsedError; + List get linkedTasks => throw _privateConstructorUsedError; /// Serializes this ChecklistData to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -39,7 +40,10 @@ abstract class $ChecklistDataCopyWith<$Res> { ChecklistData value, $Res Function(ChecklistData) then) = _$ChecklistDataCopyWithImpl<$Res, ChecklistData>; @useResult - $Res call({String title, List linkedChecklistItems}); + $Res call( + {String title, + List linkedChecklistItems, + List linkedTasks}); } /// @nodoc @@ -59,6 +63,7 @@ class _$ChecklistDataCopyWithImpl<$Res, $Val extends ChecklistData> $Res call({ Object? title = null, Object? linkedChecklistItems = null, + Object? linkedTasks = null, }) { return _then(_value.copyWith( title: null == title @@ -69,6 +74,10 @@ class _$ChecklistDataCopyWithImpl<$Res, $Val extends ChecklistData> ? _value.linkedChecklistItems : linkedChecklistItems // ignore: cast_nullable_to_non_nullable as List, + linkedTasks: null == linkedTasks + ? _value.linkedTasks + : linkedTasks // ignore: cast_nullable_to_non_nullable + as List, ) as $Val); } } @@ -81,7 +90,10 @@ abstract class _$$ChecklistDataImplCopyWith<$Res> __$$ChecklistDataImplCopyWithImpl<$Res>; @override @useResult - $Res call({String title, List linkedChecklistItems}); + $Res call( + {String title, + List linkedChecklistItems, + List linkedTasks}); } /// @nodoc @@ -99,6 +111,7 @@ class __$$ChecklistDataImplCopyWithImpl<$Res> $Res call({ Object? title = null, Object? linkedChecklistItems = null, + Object? linkedTasks = null, }) { return _then(_$ChecklistDataImpl( title: null == title @@ -109,6 +122,10 @@ class __$$ChecklistDataImplCopyWithImpl<$Res> ? _value._linkedChecklistItems : linkedChecklistItems // ignore: cast_nullable_to_non_nullable as List, + linkedTasks: null == linkedTasks + ? _value._linkedTasks + : linkedTasks // ignore: cast_nullable_to_non_nullable + as List, )); } } @@ -117,8 +134,11 @@ class __$$ChecklistDataImplCopyWithImpl<$Res> @JsonSerializable() class _$ChecklistDataImpl implements _ChecklistData { const _$ChecklistDataImpl( - {required this.title, required final List linkedChecklistItems}) - : _linkedChecklistItems = linkedChecklistItems; + {required this.title, + required final List linkedChecklistItems, + required final List linkedTasks}) + : _linkedChecklistItems = linkedChecklistItems, + _linkedTasks = linkedTasks; factory _$ChecklistDataImpl.fromJson(Map json) => _$$ChecklistDataImplFromJson(json); @@ -134,9 +154,17 @@ class _$ChecklistDataImpl implements _ChecklistData { return EqualUnmodifiableListView(_linkedChecklistItems); } + final List _linkedTasks; + @override + List get linkedTasks { + if (_linkedTasks is EqualUnmodifiableListView) return _linkedTasks; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_linkedTasks); + } + @override String toString() { - return 'ChecklistData(title: $title, linkedChecklistItems: $linkedChecklistItems)'; + return 'ChecklistData(title: $title, linkedChecklistItems: $linkedChecklistItems, linkedTasks: $linkedTasks)'; } @override @@ -146,13 +174,18 @@ class _$ChecklistDataImpl implements _ChecklistData { other is _$ChecklistDataImpl && (identical(other.title, title) || other.title == title) && const DeepCollectionEquality() - .equals(other._linkedChecklistItems, _linkedChecklistItems)); + .equals(other._linkedChecklistItems, _linkedChecklistItems) && + const DeepCollectionEquality() + .equals(other._linkedTasks, _linkedTasks)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, title, - const DeepCollectionEquality().hash(_linkedChecklistItems)); + int get hashCode => Object.hash( + runtimeType, + title, + const DeepCollectionEquality().hash(_linkedChecklistItems), + const DeepCollectionEquality().hash(_linkedTasks)); /// Create a copy of ChecklistData /// with the given fields replaced by the non-null parameter values. @@ -173,7 +206,8 @@ class _$ChecklistDataImpl implements _ChecklistData { abstract class _ChecklistData implements ChecklistData { const factory _ChecklistData( {required final String title, - required final List linkedChecklistItems}) = _$ChecklistDataImpl; + required final List linkedChecklistItems, + required final List linkedTasks}) = _$ChecklistDataImpl; factory _ChecklistData.fromJson(Map json) = _$ChecklistDataImpl.fromJson; @@ -182,6 +216,8 @@ abstract class _ChecklistData implements ChecklistData { String get title; @override List get linkedChecklistItems; + @override + List get linkedTasks; /// Create a copy of ChecklistData /// with the given fields replaced by the non-null parameter values. diff --git a/lib/classes/checklist_data.g.dart b/lib/classes/checklist_data.g.dart index 0517661bd..b8963146e 100644 --- a/lib/classes/checklist_data.g.dart +++ b/lib/classes/checklist_data.g.dart @@ -12,10 +12,14 @@ _$ChecklistDataImpl _$$ChecklistDataImplFromJson(Map json) => linkedChecklistItems: (json['linkedChecklistItems'] as List) .map((e) => e as String) .toList(), + linkedTasks: (json['linkedTasks'] as List) + .map((e) => e as String) + .toList(), ); Map _$$ChecklistDataImplToJson(_$ChecklistDataImpl instance) => { 'title': instance.title, 'linkedChecklistItems': instance.linkedChecklistItems, + 'linkedTasks': instance.linkedTasks, }; diff --git a/lib/classes/task.dart b/lib/classes/task.dart index 0662b3ec4..cbd819606 100644 --- a/lib/classes/task.dart +++ b/lib/classes/task.dart @@ -87,6 +87,7 @@ class TaskData with _$TaskData { required String title, DateTime? due, Duration? estimate, + List? checklistIds, }) = _TaskData; factory TaskData.fromJson(Map json) => diff --git a/lib/classes/task.freezed.dart b/lib/classes/task.freezed.dart index 25fce82ff..1e0bc6e08 100644 --- a/lib/classes/task.freezed.dart +++ b/lib/classes/task.freezed.dart @@ -2775,6 +2775,7 @@ mixin _$TaskData { String get title => throw _privateConstructorUsedError; DateTime? get due => throw _privateConstructorUsedError; Duration? get estimate => throw _privateConstructorUsedError; + List? get checklistIds => throw _privateConstructorUsedError; /// Serializes this TaskData to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -2798,7 +2799,8 @@ abstract class $TaskDataCopyWith<$Res> { List statusHistory, String title, DateTime? due, - Duration? estimate}); + Duration? estimate, + List? checklistIds}); $TaskStatusCopyWith<$Res> get status; } @@ -2825,6 +2827,7 @@ class _$TaskDataCopyWithImpl<$Res, $Val extends TaskData> Object? title = null, Object? due = freezed, Object? estimate = freezed, + Object? checklistIds = freezed, }) { return _then(_value.copyWith( status: null == status @@ -2855,6 +2858,10 @@ class _$TaskDataCopyWithImpl<$Res, $Val extends TaskData> ? _value.estimate : estimate // ignore: cast_nullable_to_non_nullable as Duration?, + checklistIds: freezed == checklistIds + ? _value.checklistIds + : checklistIds // ignore: cast_nullable_to_non_nullable + as List?, ) as $Val); } @@ -2884,7 +2891,8 @@ abstract class _$$TaskDataImplCopyWith<$Res> List statusHistory, String title, DateTime? due, - Duration? estimate}); + Duration? estimate, + List? checklistIds}); @override $TaskStatusCopyWith<$Res> get status; @@ -2910,6 +2918,7 @@ class __$$TaskDataImplCopyWithImpl<$Res> Object? title = null, Object? due = freezed, Object? estimate = freezed, + Object? checklistIds = freezed, }) { return _then(_$TaskDataImpl( status: null == status @@ -2940,6 +2949,10 @@ class __$$TaskDataImplCopyWithImpl<$Res> ? _value.estimate : estimate // ignore: cast_nullable_to_non_nullable as Duration?, + checklistIds: freezed == checklistIds + ? _value._checklistIds + : checklistIds // ignore: cast_nullable_to_non_nullable + as List?, )); } } @@ -2954,8 +2967,10 @@ class _$TaskDataImpl implements _TaskData { required final List statusHistory, required this.title, this.due, - this.estimate}) - : _statusHistory = statusHistory; + this.estimate, + final List? checklistIds}) + : _statusHistory = statusHistory, + _checklistIds = checklistIds; factory _$TaskDataImpl.fromJson(Map json) => _$$TaskDataImplFromJson(json); @@ -2980,10 +2995,19 @@ class _$TaskDataImpl implements _TaskData { final DateTime? due; @override final Duration? estimate; + final List? _checklistIds; + @override + List? get checklistIds { + final value = _checklistIds; + if (value == null) return null; + if (_checklistIds is EqualUnmodifiableListView) return _checklistIds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } @override String toString() { - return 'TaskData(status: $status, dateFrom: $dateFrom, dateTo: $dateTo, statusHistory: $statusHistory, title: $title, due: $due, estimate: $estimate)'; + return 'TaskData(status: $status, dateFrom: $dateFrom, dateTo: $dateTo, statusHistory: $statusHistory, title: $title, due: $due, estimate: $estimate, checklistIds: $checklistIds)'; } @override @@ -3000,7 +3024,9 @@ class _$TaskDataImpl implements _TaskData { (identical(other.title, title) || other.title == title) && (identical(other.due, due) || other.due == due) && (identical(other.estimate, estimate) || - other.estimate == estimate)); + other.estimate == estimate) && + const DeepCollectionEquality() + .equals(other._checklistIds, _checklistIds)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -3013,7 +3039,8 @@ class _$TaskDataImpl implements _TaskData { const DeepCollectionEquality().hash(_statusHistory), title, due, - estimate); + estimate, + const DeepCollectionEquality().hash(_checklistIds)); /// Create a copy of TaskData /// with the given fields replaced by the non-null parameter values. @@ -3039,7 +3066,8 @@ abstract class _TaskData implements TaskData { required final List statusHistory, required final String title, final DateTime? due, - final Duration? estimate}) = _$TaskDataImpl; + final Duration? estimate, + final List? checklistIds}) = _$TaskDataImpl; factory _TaskData.fromJson(Map json) = _$TaskDataImpl.fromJson; @@ -3058,6 +3086,8 @@ abstract class _TaskData implements TaskData { DateTime? get due; @override Duration? get estimate; + @override + List? get checklistIds; /// Create a copy of TaskData /// with the given fields replaced by the non-null parameter values. diff --git a/lib/classes/task.g.dart b/lib/classes/task.g.dart index 596136428..0799879e7 100644 --- a/lib/classes/task.g.dart +++ b/lib/classes/task.g.dart @@ -200,6 +200,9 @@ _$TaskDataImpl _$$TaskDataImplFromJson(Map json) => estimate: json['estimate'] == null ? null : Duration(microseconds: (json['estimate'] as num).toInt()), + checklistIds: (json['checklistIds'] as List?) + ?.map((e) => e as String) + .toList(), ); Map _$$TaskDataImplToJson(_$TaskDataImpl instance) => @@ -211,4 +214,5 @@ Map _$$TaskDataImplToJson(_$TaskDataImpl instance) => 'title': instance.title, 'due': instance.due?.toIso8601String(), 'estimate': instance.estimate?.inMicroseconds, + 'checklistIds': instance.checklistIds, }; diff --git a/lib/features/journal/ui/widgets/entry_details_widget.dart b/lib/features/journal/ui/widgets/entry_details_widget.dart index b48313a74..a9b3dc437 100644 --- a/lib/features/journal/ui/widgets/entry_details_widget.dart +++ b/lib/features/journal/ui/widgets/entry_details_widget.dart @@ -14,6 +14,7 @@ import 'package:lotti/features/journal/ui/widgets/entry_image_widget.dart'; import 'package:lotti/features/journal/ui/widgets/journal_card.dart'; import 'package:lotti/features/journal/ui/widgets/tags/tags_list_widget.dart'; import 'package:lotti/features/speech/ui/widgets/audio_player.dart'; +import 'package:lotti/features/tasks/ui/checklist_widget.dart'; import 'package:lotti/features/tasks/ui/task_form.dart'; import 'package:lotti/widgets/events/event_form.dart'; @@ -136,6 +137,7 @@ class EntryDetailsContent extends ConsumerWidget { event: (_) => const SizedBox.shrink(), quantitative: (_) => const SizedBox.shrink(), workout: (_) => const SizedBox.shrink(), + checklist: (_) => const SizedBox.shrink(), orElse: () { return EditorWidget( entryId: itemId, @@ -162,7 +164,9 @@ class EntryDetailsContent extends ConsumerWidget { ), journalEntry: (_) => const SizedBox.shrink(), journalImage: (_) => const SizedBox.shrink(), - checklist: (_) => const SizedBox.shrink(), + checklist: (checklist) => ChecklistWrapper( + entryId: checklist.meta.id, + ), checklistItem: (_) => const SizedBox.shrink(), ), EntryDetailFooter( diff --git a/lib/features/tasks/state/checklist_controller.dart b/lib/features/tasks/state/checklist_controller.dart new file mode 100644 index 000000000..b045d5b85 --- /dev/null +++ b/lib/features/tasks/state/checklist_controller.dart @@ -0,0 +1,70 @@ +import 'dart:async'; + +import 'package:lotti/classes/journal_entities.dart'; +import 'package:lotti/database/database.dart'; +import 'package:lotti/get_it.dart'; +import 'package:lotti/logic/persistence_logic.dart'; +import 'package:lotti/services/db_notification.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'checklist_controller.g.dart'; + +@riverpod +class ChecklistController extends _$ChecklistController { + ChecklistController() { + listen(); + } + late final String entryId; + StreamSubscription>? _updateSubscription; + final _persistenceLogic = getIt(); + + void listen() { + _updateSubscription = + getIt().updateStream.listen((affectedIds) async { + if (affectedIds.contains(entryId)) { + final latest = await _fetch(); + if (latest != state.value && latest is Checklist) { + state = AsyncData(latest); + } + } + }); + } + + @override + Future build({required String id}) async { + entryId = id; + ref.onDispose(() => _updateSubscription?.cancel()); + final entry = await _fetch(); + + if (entry is Checklist) { + return entry; + } else { + return null; + } + } + + Future _fetch() async { + return getIt().journalEntityById(entryId); + } + + Future delete() async { + final res = await _persistenceLogic.deleteJournalEntity(entryId); + state = const AsyncData(null); + return res; + } + + Future updateTitle(String? title) async { + final current = state.value; + final data = current?.data; + if (current != null && data != null) { + final updated = current.copyWith( + data: data.copyWith(title: title ?? ''), + ); + await _persistenceLogic.updateChecklist( + checklistId: entryId, + title: title ?? '', + ); + state = AsyncData(updated); + } + } +} diff --git a/lib/features/tasks/state/checklist_controller.g.dart b/lib/features/tasks/state/checklist_controller.g.dart new file mode 100644 index 000000000..d7cb2d1da --- /dev/null +++ b/lib/features/tasks/state/checklist_controller.g.dart @@ -0,0 +1,178 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'checklist_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$checklistControllerHash() => + r'8ac67a972c1983b45b76908cbc1678c265234f0b'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ChecklistController + extends BuildlessAutoDisposeAsyncNotifier { + late final String id; + + FutureOr build({ + required String id, + }); +} + +/// See also [ChecklistController]. +@ProviderFor(ChecklistController) +const checklistControllerProvider = ChecklistControllerFamily(); + +/// See also [ChecklistController]. +class ChecklistControllerFamily extends Family> { + /// See also [ChecklistController]. + const ChecklistControllerFamily(); + + /// See also [ChecklistController]. + ChecklistControllerProvider call({ + required String id, + }) { + return ChecklistControllerProvider( + id: id, + ); + } + + @override + ChecklistControllerProvider getProviderOverride( + covariant ChecklistControllerProvider provider, + ) { + return call( + id: provider.id, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'checklistControllerProvider'; +} + +/// See also [ChecklistController]. +class ChecklistControllerProvider extends AutoDisposeAsyncNotifierProviderImpl< + ChecklistController, Checklist?> { + /// See also [ChecklistController]. + ChecklistControllerProvider({ + required String id, + }) : this._internal( + () => ChecklistController()..id = id, + from: checklistControllerProvider, + name: r'checklistControllerProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$checklistControllerHash, + dependencies: ChecklistControllerFamily._dependencies, + allTransitiveDependencies: + ChecklistControllerFamily._allTransitiveDependencies, + id: id, + ); + + ChecklistControllerProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.id, + }) : super.internal(); + + final String id; + + @override + FutureOr runNotifierBuild( + covariant ChecklistController notifier, + ) { + return notifier.build( + id: id, + ); + } + + @override + Override overrideWith(ChecklistController Function() create) { + return ProviderOverride( + origin: this, + override: ChecklistControllerProvider._internal( + () => create()..id = id, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + id: id, + ), + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement + createElement() { + return _ChecklistControllerProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ChecklistControllerProvider && other.id == id; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, id.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin ChecklistControllerRef + on AutoDisposeAsyncNotifierProviderRef { + /// The parameter `id` of this provider. + String get id; +} + +class _ChecklistControllerProviderElement + extends AutoDisposeAsyncNotifierProviderElement with ChecklistControllerRef { + _ChecklistControllerProviderElement(super.provider); + + @override + String get id => (origin as ChecklistControllerProvider).id; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/tasks/ui/checklist_widget.dart b/lib/features/tasks/ui/checklist_widget.dart index 8af9f4daf..c82fe31b4 100644 --- a/lib/features/tasks/ui/checklist_widget.dart +++ b/lib/features/tasks/ui/checklist_widget.dart @@ -1,15 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lotti/features/tasks/state/checklist_controller.dart'; import 'package:lotti/features/tasks/ui/checkbox_item_wrapper.dart'; import 'package:lotti/features/tasks/ui/consts.dart'; import 'package:lotti/features/tasks/ui/title_text_field.dart'; class ChecklistWidget extends StatefulWidget { const ChecklistWidget({ + required this.title, required this.itemIds, + this.onTitleSave, super.key, }); + final String title; final List itemIds; + final StringCallback? onTitleSave; @override State createState() => _ChecklistWidgetState(); @@ -21,14 +27,15 @@ class _ChecklistWidgetState extends State { @override Widget build(BuildContext context) { return ExpansionTile( + initiallyExpanded: true, title: AnimatedCrossFade( duration: checklistCrossFadeDuration, firstChild: Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: TitleTextField( - initialValue: 'Checklist', + initialValue: widget.title, onSave: (title) { - debugPrint('Saved: $title'); + widget.onTitleSave?.call(title); setState(() { _isEditing = false; }); @@ -43,7 +50,7 @@ class _ChecklistWidgetState extends State { ), secondChild: Row( children: [ - const Text('Checklist'), + Text(widget.title), IconButton( icon: const Icon( Icons.edit, @@ -73,6 +80,7 @@ class _ChecklistWidgetState extends State { child: TitleTextField( onSave: (title) { debugPrint('Saved: $title'); + widget.onTitleSave?.call(title); }, clearOnSave: true, semanticsLabel: 'Add item to checklist', @@ -83,3 +91,29 @@ class _ChecklistWidgetState extends State { ); } } + +class ChecklistWrapper extends ConsumerWidget { + const ChecklistWrapper({ + required this.entryId, + super.key, + }); + + final String entryId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = checklistControllerProvider(id: entryId); + final notifier = ref.read(provider.notifier); + final checklist = ref.watch(provider).value; + + if (checklist == null) { + return const SizedBox.shrink(); + } + + return ChecklistWidget( + title: checklist.data.title, + itemIds: checklist.data.linkedChecklistItems, + onTitleSave: notifier.updateTitle, + ); + } +} diff --git a/lib/features/tasks/ui/checklists_widget.dart b/lib/features/tasks/ui/checklists_widget.dart new file mode 100644 index 000000000..522a309fc --- /dev/null +++ b/lib/features/tasks/ui/checklists_widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lotti/classes/journal_entities.dart'; +import 'package:lotti/features/journal/state/entry_controller.dart'; +import 'package:lotti/features/tasks/ui/checklist_widget.dart'; +import 'package:lotti/l10n/app_localizations_context.dart'; +import 'package:lotti/logic/create/create_entry.dart'; + +class ChecklistsWidget extends ConsumerWidget { + const ChecklistsWidget({ + required this.entryId, + super.key, + required this.task, + }); + + final String entryId; + final Task task; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = entryControllerProvider(id: entryId); + final item = ref.watch(provider).value?.entry; + + if (item == null || item is! Task) { + return const SizedBox.shrink(); + } + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(context.messages.checklistsTitle), + IconButton( + tooltip: context.messages.addActionAddChecklist, + onPressed: () => createChecklist(task: task), + icon: const Icon(Icons.add_rounded), + ), + ], + ), + ...?item.data.checklistIds?.map( + (checklistId) => ChecklistWrapper(entryId: checklistId), + ), + ], + ); + } +} diff --git a/lib/features/tasks/ui/task_form.dart b/lib/features/tasks/ui/task_form.dart index c57f262fa..debffb1a9 100644 --- a/lib/features/tasks/ui/task_form.dart +++ b/lib/features/tasks/ui/task_form.dart @@ -12,6 +12,8 @@ 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, { @@ -193,6 +195,9 @@ class _TaskFormState extends ConsumerState { ), ), EditorWidget(entryId: entryId), + const SizedBox(height: 10), + ChecklistsWidget(entryId: entryId, task: widget.task), + const SizedBox(height: 20), ], ); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 65b2a21a4..e4f926e1b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -12,6 +12,8 @@ "addActionAddTask": "Add Task", "addActionAddText": "Add Text Entry", "addActionAddTimeRecording": "Start Time Recording", + "addActionAddChecklist": "Add Checklist", + "checklistsTitle": "Checklists", "addAudioTitle": "Audio Recording", "addEntryTitle": "Add Text Entry", "addHabitCommentLabel": "Comment", diff --git a/lib/logic/create/create_entry.dart b/lib/logic/create/create_entry.dart index 84a269ce1..5bc15dc57 100644 --- a/lib/logic/create/create_entry.dart +++ b/lib/logic/create/create_entry.dart @@ -24,6 +24,14 @@ Future createTextEntry({String? linkedId}) async { return entry; } +Future createChecklist({required Task task}) async { + final entry = await getIt().createChecklist( + task: task, + ); + + return entry; +} + Future createTimerEntry({JournalEntity? linked}) async { final timerItem = await createTextEntry(linkedId: linked?.meta.id); if (linked != null) { diff --git a/lib/logic/persistence_logic.dart b/lib/logic/persistence_logic.dart index 9abbfa681..c09690ec2 100644 --- a/lib/logic/persistence_logic.dart +++ b/lib/logic/persistence_logic.dart @@ -4,6 +4,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/entity_definitions.dart'; import 'package:lotti/classes/entry_links.dart'; import 'package:lotti/classes/entry_text.dart'; @@ -522,6 +523,107 @@ class PersistenceLogic { } } + Future createChecklist({ + required Task task, + }) async { + try { + final now = DateTime.now(); + final id = uuid.v1(); + final vc = await _vectorClockService.getNextVectorClock(); + + final newChecklist = JournalEntity.checklist( + meta: Metadata( + createdAt: now, + updatedAt: now, + dateFrom: now, + dateTo: now, + id: id, + vectorClock: vc, + timezone: await getLocalTimezone(), + utcOffset: now.timeZoneOffset.inMinutes, + ), + data: ChecklistData( + title: 'Checklist: ${task.data.title}', + linkedChecklistItems: [], + linkedTasks: [task.id], + ), + ); + await createDbEntity( + newChecklist, + enqueueSync: true, + ); + addGeolocation(id); + + await updateTask( + journalEntityId: task.id, + taskData: task.data.copyWith( + checklistIds: [ + ...?task.data.checklistIds, + newChecklist.meta.id, + ], + ), + ); + + return newChecklist; + } catch (exception, stackTrace) { + _loggingDb.captureException( + exception, + domain: 'persistence_logic', + subDomain: 'createChecklistEntry', + stackTrace: stackTrace, + ); + return null; + } + } + + Future updateChecklist({ + required String checklistId, + required String title, + }) async { + try { + final now = DateTime.now(); + final journalEntity = await _journalDb.journalEntityById(checklistId); + + if (journalEntity == null) { + return false; + } + + await journalEntity.maybeMap( + checklist: (Checklist checklist) async { + final vc = await _vectorClockService.getNextVectorClock( + previous: journalEntity.meta.vectorClock, + ); + + final oldMeta = journalEntity.meta; + final newMeta = oldMeta.copyWith( + updatedAt: now, + vectorClock: vc, + ); + + final newTask = checklist.copyWith( + meta: newMeta, + data: checklist.data.copyWith(title: title), + ); + + await updateDbEntity(newTask, enqueueSync: true); + }, + orElse: () async => _loggingDb.captureException( + 'not a checklist', + domain: 'persistence_logic', + subDomain: 'updateChecklist', + ), + ); + } catch (exception, stackTrace) { + _loggingDb.captureException( + exception, + domain: 'persistence_logic', + subDomain: 'updateChecklist', + stackTrace: stackTrace, + ); + } + return true; + } + Future createLink({ required String fromId, required String toId, diff --git a/lib/widgetbook.dart b/lib/widgetbook.dart index 2d847d608..c5eba0d8e 100644 --- a/lib/widgetbook.dart +++ b/lib/widgetbook.dart @@ -119,6 +119,7 @@ class WidgetbookApp extends StatelessWidget { ), ], child: ChecklistWidget( + title: 'Checklist', itemIds: [ checklistItem1.meta.id, checklistItem2.meta.id, diff --git a/lib/widgets/create/add_actions.dart b/lib/widgets/create/add_actions.dart index d8cdf057a..a8ecbecf3 100644 --- a/lib/widgets/create/add_actions.dart +++ b/lib/widgets/create/add_actions.dart @@ -176,6 +176,24 @@ class _RadialAddActionButtonsState extends State { ), ); + final linkedItem = widget.linked; + if (linkedItem is Task) { + items.add( + FloatingActionButton( + heroTag: 'Checklist', + tooltip: context.messages.addActionAddChecklist, + onPressed: () async { + rebuild(); + await createChecklist(task: linkedItem); + }, + child: const Icon( + Icons.checklist_rounded, + size: actionIconSize, + ), + ), + ); + } + return Padding( padding: const EdgeInsets.only(right: 1, bottom: 1.5), child: CircleFloatingButton.floatingActionButton(