Implement WhatsApp share intake and person quick actions
This commit is contained in:
@@ -23,6 +23,8 @@ selectedPersonIdProvider = NotifierProvider<SelectedPersonIdNotifier, String?>(
|
||||
SelectedPersonIdNotifier.new,
|
||||
);
|
||||
|
||||
enum _PersonQuickAction { capture, note, idea, reminder }
|
||||
|
||||
class PeopleView extends ConsumerWidget {
|
||||
const PeopleView({super.key});
|
||||
|
||||
@@ -81,6 +83,7 @@ class PeopleView extends ConsumerWidget {
|
||||
relationship: draft.relationship,
|
||||
notes: draft.notes,
|
||||
tags: draft.tags,
|
||||
location: draft.location,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,6 +101,7 @@ class PeopleView extends ConsumerWidget {
|
||||
relationship: person.relationship,
|
||||
notes: person.notes,
|
||||
tags: person.tags,
|
||||
location: person.location,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -114,6 +118,7 @@ class PeopleView extends ConsumerWidget {
|
||||
relationship: draft.relationship,
|
||||
notes: draft.notes,
|
||||
tags: draft.tags,
|
||||
location: draft.location,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -202,6 +207,8 @@ class PeopleView extends ConsumerWidget {
|
||||
person: selected,
|
||||
onEdit: () => _handleEditPerson(context, ref, selected),
|
||||
onDelete: () => _handleDeletePerson(context, ref, selected.id),
|
||||
onQuickAction: (_PersonQuickAction action) =>
|
||||
_handleQuickAction(context, ref, selected, action),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -244,12 +251,125 @@ class PeopleView extends ConsumerWidget {
|
||||
compact: true,
|
||||
onEdit: () => _handleEditPerson(context, ref, selected),
|
||||
onDelete: () => _handleDeletePerson(context, ref, selected.id),
|
||||
onQuickAction: (_PersonQuickAction action) =>
|
||||
_handleQuickAction(context, ref, selected, action),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleQuickAction(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
PersonProfile person,
|
||||
_PersonQuickAction action,
|
||||
) async {
|
||||
switch (action) {
|
||||
case _PersonQuickAction.capture:
|
||||
final String? summary = await _showQuickTextCaptureDialog(
|
||||
context,
|
||||
title: 'Add Capture',
|
||||
hintText: 'What happened in this interaction?',
|
||||
);
|
||||
if (summary == null) {
|
||||
return;
|
||||
}
|
||||
await ref
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addMoment(personId: person.id, summary: summary, type: 'capture');
|
||||
return;
|
||||
case _PersonQuickAction.note:
|
||||
final String? note = await _showQuickTextCaptureDialog(
|
||||
context,
|
||||
title: 'Add Note',
|
||||
hintText: 'Write a quick context note...',
|
||||
);
|
||||
if (note == null) {
|
||||
return;
|
||||
}
|
||||
await ref
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addMoment(personId: person.id, summary: note, type: 'note');
|
||||
return;
|
||||
case _PersonQuickAction.idea:
|
||||
final _QuickIdeaDraft? idea = await showDialog<_QuickIdeaDraft>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => const _QuickIdeaDialog(),
|
||||
);
|
||||
if (idea == null) {
|
||||
return;
|
||||
}
|
||||
await ref
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addIdea(
|
||||
type: idea.type,
|
||||
title: idea.title,
|
||||
details: idea.details,
|
||||
personId: person.id,
|
||||
);
|
||||
return;
|
||||
case _PersonQuickAction.reminder:
|
||||
final _QuickReminderDraft? reminder =
|
||||
await showDialog<_QuickReminderDraft>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => const _QuickReminderDialog(),
|
||||
);
|
||||
if (reminder == null) {
|
||||
return;
|
||||
}
|
||||
await ref
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addReminder(
|
||||
title: reminder.title,
|
||||
cadence: reminder.cadence,
|
||||
nextAt: reminder.nextAt,
|
||||
personId: person.id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _showQuickTextCaptureDialog(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String hintText,
|
||||
}) async {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
final String? result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(hintText: hintText),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final String value = controller.text.trim();
|
||||
if (value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(value);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
controller.dispose();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyPeopleView extends StatelessWidget {
|
||||
@@ -294,12 +414,14 @@ class _PersonDetail extends StatelessWidget {
|
||||
required this.person,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
required this.onQuickAction,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
final PersonProfile person;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
final ValueChanged<_PersonQuickAction> onQuickAction;
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
@@ -341,6 +463,30 @@ class _PersonDetail extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
PopupMenuButton<_PersonQuickAction>(
|
||||
tooltip: 'Quick add',
|
||||
onSelected: onQuickAction,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
const <PopupMenuEntry<_PersonQuickAction>>[
|
||||
PopupMenuItem<_PersonQuickAction>(
|
||||
value: _PersonQuickAction.capture,
|
||||
child: Text('Add Capture'),
|
||||
),
|
||||
PopupMenuItem<_PersonQuickAction>(
|
||||
value: _PersonQuickAction.note,
|
||||
child: Text('Add Note'),
|
||||
),
|
||||
PopupMenuItem<_PersonQuickAction>(
|
||||
value: _PersonQuickAction.idea,
|
||||
child: Text('Add Idea'),
|
||||
),
|
||||
PopupMenuItem<_PersonQuickAction>(
|
||||
value: _PersonQuickAction.reminder,
|
||||
child: Text('Add Reminder'),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.add_circle_outline_rounded),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onEdit,
|
||||
icon: const Icon(Icons.edit_rounded),
|
||||
@@ -382,6 +528,30 @@ class _PersonDetail extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<_PersonQuickAction>(
|
||||
tooltip: 'Quick add',
|
||||
onSelected: onQuickAction,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
const <PopupMenuEntry<_PersonQuickAction>>[
|
||||
PopupMenuItem<_PersonQuickAction>(
|
||||
value: _PersonQuickAction.capture,
|
||||
child: Text('Add Capture'),
|
||||
),
|
||||
PopupMenuItem<_PersonQuickAction>(
|
||||
value: _PersonQuickAction.note,
|
||||
child: Text('Add Note'),
|
||||
),
|
||||
PopupMenuItem<_PersonQuickAction>(
|
||||
value: _PersonQuickAction.idea,
|
||||
child: Text('Add Idea'),
|
||||
),
|
||||
PopupMenuItem<_PersonQuickAction>(
|
||||
value: _PersonQuickAction.reminder,
|
||||
child: Text('Add Reminder'),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.add_circle_outline_rounded),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onEdit,
|
||||
icon: const Icon(Icons.edit_rounded),
|
||||
@@ -394,6 +564,12 @@ class _PersonDetail extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
if ((person.location ?? '').trim().isNotEmpty) ...<Widget>[
|
||||
SizedBox(height: compact ? 10 : 14),
|
||||
Text('Location', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
_DetailTag(label: person.location!.trim()),
|
||||
],
|
||||
SizedBox(height: compact ? 14 : 22),
|
||||
Text('Next moment', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
@@ -555,6 +731,7 @@ class _PersonEditorDialog extends StatefulWidget {
|
||||
class _PersonEditorDialogState extends State<_PersonEditorDialog> {
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _relationshipController;
|
||||
late final TextEditingController _locationController;
|
||||
late final TextEditingController _tagsController;
|
||||
late final TextEditingController _notesController;
|
||||
|
||||
@@ -565,6 +742,9 @@ class _PersonEditorDialogState extends State<_PersonEditorDialog> {
|
||||
_relationshipController = TextEditingController(
|
||||
text: widget.initial?.relationship ?? '',
|
||||
);
|
||||
_locationController = TextEditingController(
|
||||
text: widget.initial?.location ?? '',
|
||||
);
|
||||
_tagsController = TextEditingController(
|
||||
text: widget.initial?.tags.join(', ') ?? '',
|
||||
);
|
||||
@@ -575,6 +755,7 @@ class _PersonEditorDialogState extends State<_PersonEditorDialog> {
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_relationshipController.dispose();
|
||||
_locationController.dispose();
|
||||
_tagsController.dispose();
|
||||
_notesController.dispose();
|
||||
super.dispose();
|
||||
@@ -598,6 +779,12 @@ class _PersonEditorDialogState extends State<_PersonEditorDialog> {
|
||||
controller: _relationshipController,
|
||||
decoration: const InputDecoration(labelText: 'Relationship'),
|
||||
),
|
||||
TextField(
|
||||
controller: _locationController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Location (optional)',
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _tagsController,
|
||||
decoration: const InputDecoration(
|
||||
@@ -629,6 +816,9 @@ class _PersonEditorDialogState extends State<_PersonEditorDialog> {
|
||||
final _PersonDraft draft = _PersonDraft(
|
||||
name: name,
|
||||
relationship: relationship,
|
||||
location: _locationController.text.trim().isEmpty
|
||||
? null
|
||||
: _locationController.text.trim(),
|
||||
notes: _notesController.text.trim(),
|
||||
tags: _tagsController.text
|
||||
.split(',')
|
||||
@@ -651,12 +841,250 @@ class _PersonDraft {
|
||||
required this.relationship,
|
||||
required this.notes,
|
||||
required this.tags,
|
||||
this.location,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String relationship;
|
||||
final String notes;
|
||||
final List<String> tags;
|
||||
final String? location;
|
||||
}
|
||||
|
||||
class _QuickIdeaDraft {
|
||||
const _QuickIdeaDraft({
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.details,
|
||||
});
|
||||
|
||||
final IdeaType type;
|
||||
final String title;
|
||||
final String details;
|
||||
}
|
||||
|
||||
class _QuickIdeaDialog extends StatefulWidget {
|
||||
const _QuickIdeaDialog();
|
||||
|
||||
@override
|
||||
State<_QuickIdeaDialog> createState() => _QuickIdeaDialogState();
|
||||
}
|
||||
|
||||
class _QuickIdeaDialogState extends State<_QuickIdeaDialog> {
|
||||
IdeaType _type = IdeaType.gift;
|
||||
final TextEditingController _titleController = TextEditingController();
|
||||
final TextEditingController _detailsController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_detailsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Add Idea'),
|
||||
content: SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SegmentedButton<IdeaType>(
|
||||
segments: const <ButtonSegment<IdeaType>>[
|
||||
ButtonSegment<IdeaType>(
|
||||
value: IdeaType.gift,
|
||||
label: Text('Gift'),
|
||||
),
|
||||
ButtonSegment<IdeaType>(
|
||||
value: IdeaType.event,
|
||||
label: Text('Event'),
|
||||
),
|
||||
],
|
||||
selected: <IdeaType>{_type},
|
||||
onSelectionChanged: (Set<IdeaType> values) {
|
||||
setState(() {
|
||||
_type = values.first;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _titleController,
|
||||
decoration: const InputDecoration(labelText: 'Title'),
|
||||
),
|
||||
TextField(
|
||||
controller: _detailsController,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(labelText: 'Details'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final String title = _titleController.text.trim();
|
||||
if (title.isEmpty) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(
|
||||
_QuickIdeaDraft(
|
||||
type: _type,
|
||||
title: title,
|
||||
details: _detailsController.text.trim(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Add'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickReminderDraft {
|
||||
const _QuickReminderDraft({
|
||||
required this.title,
|
||||
required this.cadence,
|
||||
required this.nextAt,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final ReminderCadence cadence;
|
||||
final DateTime nextAt;
|
||||
}
|
||||
|
||||
class _QuickReminderDialog extends StatefulWidget {
|
||||
const _QuickReminderDialog();
|
||||
|
||||
@override
|
||||
State<_QuickReminderDialog> createState() => _QuickReminderDialogState();
|
||||
}
|
||||
|
||||
class _QuickReminderDialogState extends State<_QuickReminderDialog> {
|
||||
final TextEditingController _titleController = TextEditingController();
|
||||
ReminderCadence _cadence = ReminderCadence.weekly;
|
||||
DateTime _nextAt = DateTime.now().add(const Duration(days: 1));
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Add Reminder'),
|
||||
content: SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _titleController,
|
||||
decoration: const InputDecoration(labelText: 'Title'),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
DropdownButtonFormField<ReminderCadence>(
|
||||
initialValue: _cadence,
|
||||
decoration: const InputDecoration(labelText: 'Cadence'),
|
||||
items: ReminderCadence.values
|
||||
.map(
|
||||
(ReminderCadence cadence) =>
|
||||
DropdownMenuItem<ReminderCadence>(
|
||||
value: cadence,
|
||||
child: Text(cadence.name),
|
||||
),
|
||||
)
|
||||
.toList(growable: false),
|
||||
onChanged: (ReminderCadence? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_cadence = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Next: ${_nextAt.year}-${_nextAt.month.toString().padLeft(2, '0')}-${_nextAt.day.toString().padLeft(2, '0')} ${_nextAt.hour.toString().padLeft(2, '0')}:${_nextAt.minute.toString().padLeft(2, '0')}',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime? date = await showDatePicker(
|
||||
context: context,
|
||||
firstDate: now,
|
||||
lastDate: now.add(const Duration(days: 3650)),
|
||||
initialDate: _nextAt,
|
||||
);
|
||||
if (date == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
final TimeOfDay? time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(_nextAt),
|
||||
);
|
||||
if (time == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_nextAt = DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: const Text('Change'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final String title = _titleController.text.trim();
|
||||
if (title.isEmpty) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop(
|
||||
_QuickReminderDraft(
|
||||
title: title,
|
||||
cadence: _cadence,
|
||||
nextAt: _nextAt,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Add'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DetailTag extends StatelessWidget {
|
||||
|
||||
Reference in New Issue
Block a user