part of 'people_view.dart'; Future _showPersonFactEditor( BuildContext context, { required PersonFact fact, }) { return showDialog( context: context, builder: (BuildContext context) => _PersonFactEditorDialog(fact: fact), ); } Future _showImportantDateEditor( BuildContext context, { required PersonImportantDate value, }) { return showDialog( context: context, builder: (BuildContext context) => _ImportantDateEditorDialog(value: value), ); } class _PersonFactEditorDialog extends StatefulWidget { const _PersonFactEditorDialog({required this.fact}); final PersonFact fact; @override State<_PersonFactEditorDialog> createState() => _PersonFactEditorDialogState(); } class _PersonFactEditorDialogState extends State<_PersonFactEditorDialog> { late CapturedFactType _type; late final TextEditingController _labelController; late final TextEditingController _textController; bool _isSensitive = false; @override void initState() { super.initState(); _type = widget.fact.type; _labelController = TextEditingController(text: widget.fact.label ?? ''); _textController = TextEditingController(text: widget.fact.text); _isSensitive = widget.fact.isSensitive; } @override void dispose() { _labelController.dispose(); _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Edit Captured Fact'), content: SingleChildScrollView( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 460), child: Column( mainAxisSize: MainAxisSize.min, children: [ DropdownButtonFormField( initialValue: _type, decoration: const InputDecoration(labelText: 'Type'), items: CapturedFactType.values .where( (CapturedFactType type) => type != CapturedFactType.importantDate, ) .map( (CapturedFactType type) => DropdownMenuItem( value: type, child: Text(_factTypeLabel(type)), ), ) .toList(growable: false), onChanged: (CapturedFactType? value) { if (value == null) { return; } setState(() { _type = value; }); }, ), const SizedBox(height: 10), TextField( controller: _labelController, decoration: const InputDecoration(labelText: 'Label'), ), const SizedBox(height: 10), TextField( controller: _textController, minLines: 2, maxLines: 4, decoration: const InputDecoration(labelText: 'Text'), ), const SizedBox(height: 10), SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, title: const Text('Sensitive'), value: _isSensitive, onChanged: (bool value) { setState(() { _isSensitive = value; }); }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { Navigator.of(context).pop( widget.fact.copyWith( type: _type, label: _labelController.text.trim().isEmpty ? null : _labelController.text.trim(), text: _textController.text.trim(), isSensitive: _isSensitive, ), ); }, child: const Text('Save'), ), ], ); } } class _ImportantDateEditorDialog extends StatefulWidget { const _ImportantDateEditorDialog({required this.value}); final PersonImportantDate value; @override State<_ImportantDateEditorDialog> createState() => _ImportantDateEditorDialogState(); } class _ImportantDateEditorDialogState extends State<_ImportantDateEditorDialog> { late final TextEditingController _labelController; late final TextEditingController _classificationController; late DateTime _date; bool _isSensitive = false; @override void initState() { super.initState(); _labelController = TextEditingController(text: widget.value.label); _classificationController = TextEditingController( text: widget.value.classification, ); _date = widget.value.date; _isSensitive = widget.value.isSensitive; } @override void dispose() { _labelController.dispose(); _classificationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Edit Important Date'), content: SingleChildScrollView( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 460), child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _labelController, decoration: const InputDecoration(labelText: 'Label'), ), const SizedBox(height: 10), TextField( controller: _classificationController, decoration: const InputDecoration(labelText: 'Classification'), ), const SizedBox(height: 10), OutlinedButton.icon( onPressed: _pickDate, icon: const Icon(Icons.event_outlined), label: Text(_dateTimeLabel(_date)), ), const SizedBox(height: 10), SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, title: const Text('Sensitive'), value: _isSensitive, onChanged: (bool value) { setState(() { _isSensitive = value; }); }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { Navigator.of(context).pop( widget.value.copyWith( label: _labelController.text.trim(), classification: _classificationController.text.trim(), date: _date, isSensitive: _isSensitive, ), ); }, child: const Text('Save'), ), ], ); } Future _pickDate() async { final DateTime? picked = await showDatePicker( context: context, firstDate: DateTime(_date.year - 5), lastDate: DateTime(_date.year + 10), initialDate: _date, ); if (picked == null) { return; } setState(() { _date = picked; }); } } class _PersonEditorDialog extends StatelessWidget { const _PersonEditorDialog({ required this.title, required this.existingPeople, this.initial, this.editingPersonId, }); final String title; final List existingPeople; final _PersonDraft? initial; final String? editingPersonId; @override Widget build(BuildContext context) { return Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: _PersonEditorPanel( title: title, initial: initial, existingPeople: existingPeople, editingPersonId: editingPersonId, compact: false, sheetStyle: false, ), ), ); } } class _QuickTextCaptureDialog extends StatefulWidget { const _QuickTextCaptureDialog({required this.title, required this.hintText}); final String title; final String hintText; @override State<_QuickTextCaptureDialog> createState() => _QuickTextCaptureDialogState(); } class _QuickTextCaptureDialogState extends State<_QuickTextCaptureDialog> { late final TextEditingController _controller; bool _attemptedSubmit = false; @override void initState() { super.initState(); _controller = TextEditingController(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final String value = _controller.text.trim(); return AlertDialog( title: Text(widget.title), content: TextField( controller: _controller, minLines: 2, maxLines: 4, autofocus: true, onChanged: (_) { if (_attemptedSubmit) { setState(() {}); } }, decoration: InputDecoration( hintText: widget.hintText, errorText: _attemptedSubmit && value.isEmpty ? 'Required' : null, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { final String submitted = _controller.text.trim(); if (submitted.isEmpty) { setState(() { _attemptedSubmit = true; }); return; } Navigator.of(context).pop(submitted); }, child: const Text('Save'), ), ], ); } } class _PersonEditorBottomSheet extends StatelessWidget { const _PersonEditorBottomSheet({ required this.title, required this.existingPeople, this.initial, this.editingPersonId, }); final String title; final List existingPeople; final _PersonDraft? initial; final String? editingPersonId; @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only( left: 8, right: 8, bottom: MediaQuery.viewInsetsOf(context).bottom + 8, ), child: _PersonEditorPanel( title: title, initial: initial, existingPeople: existingPeople, editingPersonId: editingPersonId, compact: true, sheetStyle: true, ), ); } } class _PersonEditorPanel extends StatefulWidget { const _PersonEditorPanel({ required this.title, required this.compact, required this.sheetStyle, required this.existingPeople, this.initial, this.editingPersonId, }); final String title; final bool compact; final bool sheetStyle; final List existingPeople; final _PersonDraft? initial; final String? editingPersonId; @override State<_PersonEditorPanel> createState() => _PersonEditorPanelState(); } class _PersonEditorPanelState extends State<_PersonEditorPanel> { late final TextEditingController _nameController; late final TextEditingController _relationshipController; late final TextEditingController _locationController; late final TextEditingController _aliasesController; late final TextEditingController _tagsController; late final TextEditingController _notesController; bool _attemptedSubmit = false; @override void initState() { super.initState(); _nameController = TextEditingController(text: widget.initial?.name ?? '') ..addListener(_handlePreviewChanged); _relationshipController = TextEditingController( text: widget.initial?.relationship ?? '', )..addListener(_handlePreviewChanged); _locationController = TextEditingController( text: widget.initial?.location ?? '', )..addListener(_handlePreviewChanged); _aliasesController = TextEditingController( text: widget.initial?.aliases.join(', ') ?? '', )..addListener(_handlePreviewChanged); _tagsController = TextEditingController( text: widget.initial?.tags.join(', ') ?? '', )..addListener(_handlePreviewChanged); _notesController = TextEditingController(text: widget.initial?.notes ?? ''); } @override void dispose() { _nameController ..removeListener(_handlePreviewChanged) ..dispose(); _relationshipController ..removeListener(_handlePreviewChanged) ..dispose(); _locationController ..removeListener(_handlePreviewChanged) ..dispose(); _aliasesController ..removeListener(_handlePreviewChanged) ..dispose(); _tagsController ..removeListener(_handlePreviewChanged) ..dispose(); _notesController.dispose(); super.dispose(); } void _handlePreviewChanged() { if (mounted) { setState(() {}); } } @override Widget build(BuildContext context) { final String name = _nameController.text.trim(); final String relationship = _relationshipController.text.trim(); final bool nameMissing = _attemptedSubmit && name.isEmpty; final bool relationshipMissing = _attemptedSubmit && relationship.isEmpty; final String? location = _locationController.text.trim().isEmpty ? null : _locationController.text.trim(); final List tags = _tagsController.text .split(',') .map((String tag) => tag.trim()) .where((String tag) => tag.isNotEmpty) .toList(growable: false); final List duplicateMatches = _findDuplicateNameMatches( name: name, people: widget.existingPeople, excludePersonId: widget.editingPersonId, ); final bool hasDuplicateWarning = duplicateMatches.isNotEmpty; final BorderRadius radius = BorderRadius.circular(widget.compact ? 24 : 22); final double maxHeight = widget.compact ? 720 : 760; return Material( color: Colors.transparent, child: Container( constraints: BoxConstraints( maxHeight: maxHeight, minHeight: widget.compact ? 360 : 420, ), decoration: BoxDecoration( borderRadius: radius, gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFF7FBFD), Color(0xFFEAF3F9)], ), border: Border.all(color: Colors.white.withValues(alpha: 0.95)), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), blurRadius: 26, offset: const Offset(0, 14), ), ], ), child: ClipRRect( borderRadius: radius, child: Column( children: [ if (widget.sheetStyle) Padding( padding: const EdgeInsets.only(top: 8), child: Container( width: 42, height: 4, decoration: BoxDecoration( color: const Color(0xFFCCD6DD), borderRadius: BorderRadius.circular(99), ), ), ), Expanded( child: SingleChildScrollView( padding: EdgeInsets.fromLTRB( widget.compact ? 14 : 18, widget.compact ? 10 : 16, widget.compact ? 14 : 18, 12, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _PersonEditorHero( title: widget.title, compact: widget.compact, previewName: name, previewRelationship: relationship, previewLocation: location, previewTags: tags, ), if (hasDuplicateWarning) ...[ const SizedBox(height: 10), _PersonEditorWarningBanner(matches: duplicateMatches), ], const SizedBox(height: 12), if (widget.compact) Column( children: [ _PersonEditorSectionCard( title: 'Identity', icon: Icons.person_outline_rounded, child: Column( children: [ TextField( controller: _nameController, autofocus: true, decoration: InputDecoration( labelText: 'Name', errorText: nameMissing ? 'Name is required' : null, ), ), const SizedBox(height: 10), TextField( controller: _relationshipController, decoration: InputDecoration( labelText: 'Relationship', errorText: relationshipMissing ? 'Relationship is required' : null, ), ), const SizedBox(height: 10), TextField( controller: _locationController, decoration: const InputDecoration( labelText: 'Location (optional)', ), ), const SizedBox(height: 10), TextField( controller: _aliasesController, decoration: const InputDecoration( labelText: 'Aliases (comma separated)', ), ), ], ), ), const SizedBox(height: 10), _PersonEditorSectionCard( title: 'Context', icon: Icons.auto_awesome_rounded, child: Column( children: [ TextField( controller: _tagsController, decoration: const InputDecoration( labelText: 'Tags (comma separated)', ), ), const SizedBox(height: 10), TextField( controller: _notesController, minLines: 3, maxLines: 5, decoration: const InputDecoration( labelText: 'Notes', hintText: 'Preferences, boundaries, gift clues, recurring themes...', ), ), ], ), ), ], ) else Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _PersonEditorSectionCard( title: 'Identity', icon: Icons.person_outline_rounded, child: Column( children: [ TextField( controller: _nameController, autofocus: true, decoration: InputDecoration( labelText: 'Name', errorText: nameMissing ? 'Name is required' : null, ), ), const SizedBox(height: 10), TextField( controller: _relationshipController, decoration: InputDecoration( labelText: 'Relationship', errorText: relationshipMissing ? 'Relationship is required' : null, ), ), const SizedBox(height: 10), TextField( controller: _locationController, decoration: const InputDecoration( labelText: 'Location (optional)', ), ), const SizedBox(height: 10), TextField( controller: _aliasesController, decoration: const InputDecoration( labelText: 'Aliases (comma separated)', ), ), ], ), ), ), const SizedBox(width: 12), Expanded( child: _PersonEditorSectionCard( title: 'Context', icon: Icons.auto_awesome_rounded, child: Column( children: [ TextField( controller: _tagsController, decoration: const InputDecoration( labelText: 'Tags (comma separated)', ), ), const SizedBox(height: 10), TextField( controller: _notesController, minLines: 5, maxLines: 8, decoration: const InputDecoration( labelText: 'Notes', hintText: 'Preferences, boundaries, gift clues, recurring themes...', ), ), ], ), ), ), ], ), ], ), ), ), Container( padding: EdgeInsets.fromLTRB( widget.compact ? 14 : 18, 10, widget.compact ? 14 : 18, widget.compact ? 14 : 16, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.75), border: Border( top: BorderSide(color: Colors.white.withValues(alpha: 0.9)), ), ), child: Row( children: [ Expanded( child: Text( 'Required: name and relationship', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.textSecondary, ), ), ), const SizedBox(width: 8), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), const SizedBox(width: 8), FilledButton( onPressed: () { final String submittedName = _nameController.text .trim(); final String submittedRelationship = _relationshipController.text.trim(); if (submittedName.isEmpty || submittedRelationship.isEmpty) { setState(() { _attemptedSubmit = true; }); return; } final _PersonDraft draft = _PersonDraft( name: submittedName, relationship: submittedRelationship, location: _locationController.text.trim().isEmpty ? null : _locationController.text.trim(), aliases: _aliasesController.text .split(',') .map((String value) => value.trim()) .where((String value) => value.isNotEmpty) .toList(growable: false), notes: _notesController.text.trim(), tags: _tagsController.text .split(',') .map((String tag) => tag.trim()) .where((String tag) => tag.isNotEmpty) .toList(growable: false), ); Navigator.of(context).pop(draft); }, child: const Text('Save'), ), ], ), ), ], ), ), ), ); } } class _PersonEditorHero extends StatelessWidget { const _PersonEditorHero({ required this.title, required this.compact, required this.previewName, required this.previewRelationship, required this.previewLocation, required this.previewTags, }); final String title; final bool compact; final String previewName; final String previewRelationship; final String? previewLocation; final List previewTags; @override Widget build(BuildContext context) { final String displayName = previewName.isEmpty ? 'New person' : previewName; final String displayRelationship = previewRelationship.isEmpty ? 'Relationship type' : previewRelationship; return Container( width: double.infinity, padding: EdgeInsets.all(compact ? 14 : 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: Colors.white.withValues(alpha: 0.72), border: Border.all(color: Colors.white), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ _AvatarSeed(name: displayName, size: compact ? 50 : 56), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 4), Text( 'Shape the profile first, then use quick-add from the People workspace.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.textSecondary, ), ), ], ), ), ], ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ _InlineMetaPill( icon: Icons.person_outline_rounded, label: displayName, ), _InlineMetaPill( icon: Icons.favorite_outline_rounded, label: displayRelationship, ), if (previewLocation != null) _InlineMetaPill( icon: Icons.location_on_outlined, label: previewLocation!, ), ...previewTags .take(compact ? 2 : 4) .map( (String tag) => _InlineMetaPill(icon: Icons.sell_outlined, label: tag), ), ], ), ], ), ); } } class _PersonEditorSectionCard extends StatelessWidget { const _PersonEditorSectionCard({ required this.title, required this.icon, required this.child, }); final String title; final IconData icon; final Widget child; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.78), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 18, color: AppTheme.primary), const SizedBox(width: 8), Expanded( child: Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleMedium, ), ), ], ), const SizedBox(height: 10), child, ], ), ); } } class _PersonEditorWarningBanner extends StatelessWidget { const _PersonEditorWarningBanner({required this.matches}); final List matches; @override Widget build(BuildContext context) { final String names = matches .take(2) .map( (PersonProfile person) => '${person.name} (${person.relationship})', ) .join(', '); final int extraCount = matches.length - 2; final String suffix = extraCount > 0 ? ' +$extraCount more' : ''; return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFFFF7E8), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFE2A8)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.only(top: 1), child: Icon( Icons.warning_amber_rounded, size: 18, color: Color(0xFFA56A00), ), ), const SizedBox(width: 8), Expanded( child: Text( 'Possible duplicate profile name detected. Existing: $names$suffix. You can still save, then merge duplicates from People.', style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: const Color(0xFF6F5200)), ), ), ], ), ); } } class _PersonDraft { const _PersonDraft({ required this.name, required this.relationship, required this.notes, required this.tags, required this.aliases, this.location, }); final String name; final String relationship; final String notes; final List tags; final List aliases; final String? location; } List _findDuplicateNameMatches({ required String name, required List people, String? excludePersonId, }) { final String normalized = _normalizePersonNameKey(name); if (normalized.isEmpty) { return const []; } return people .where((PersonProfile person) { if (excludePersonId != null && person.id == excludePersonId) { return false; } return _normalizePersonNameKey(person.name) == normalized; }) .toList(growable: false); } String _normalizePersonNameKey(String value) { return value .trim() .toLowerCase() .replaceAll(RegExp(r'[^a-z0-9]+'), ' ') .replaceAll(RegExp(r'\s+'), ' '); } class _MergePeopleDraft { const _MergePeopleDraft({ required this.sourcePersonId, required this.targetPersonId, }); final String sourcePersonId; final String targetPersonId; } class _MergePeopleDialog extends StatefulWidget { const _MergePeopleDialog({required this.people}); final List people; @override State<_MergePeopleDialog> createState() => _MergePeopleDialogState(); } class _MergePeopleDialogState extends State<_MergePeopleDialog> { late String _sourcePersonId; late String _targetPersonId; @override void initState() { super.initState(); _targetPersonId = widget.people.first.id; _sourcePersonId = widget.people.length > 1 ? widget.people[1].id : widget.people.first.id; } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Merge Duplicate Profiles'), content: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Move captures, ideas, reminders, and share links from source into target profile.', style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), ), const SizedBox(height: 12), DropdownButtonFormField( key: ValueKey('target-$_targetPersonId'), initialValue: _targetPersonId, decoration: const InputDecoration( labelText: 'Keep target profile', ), items: widget.people .map( (PersonProfile person) => DropdownMenuItem( value: person.id, child: Text('${person.name} · ${person.relationship}'), ), ) .toList(growable: false), onChanged: (String? value) { if (value == null) { return; } setState(() { _targetPersonId = value; if (_targetPersonId == _sourcePersonId) { _sourcePersonId = widget.people .firstWhere( (PersonProfile person) => person.id != _targetPersonId, orElse: () => widget.people.first, ) .id; } }); }, ), const SizedBox(height: 10), DropdownButtonFormField( key: ValueKey('source-$_targetPersonId-$_sourcePersonId'), initialValue: _sourcePersonId, decoration: const InputDecoration( labelText: 'Merge and remove source profile', ), items: widget.people .where((PersonProfile person) => person.id != _targetPersonId) .map( (PersonProfile person) => DropdownMenuItem( value: person.id, child: Text('${person.name} · ${person.relationship}'), ), ) .toList(growable: false), onChanged: (String? value) { if (value == null) { return; } setState(() { _sourcePersonId = value; }); }, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { if (_sourcePersonId == _targetPersonId) { return; } Navigator.of(context).pop( _MergePeopleDraft( sourcePersonId: _sourcePersonId, targetPersonId: _targetPersonId, ), ); }, child: const Text('Merge'), ), ], ); } } 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: [ SegmentedButton( segments: const >[ ButtonSegment( value: IdeaType.gift, label: Text('Gift'), ), ButtonSegment( value: IdeaType.event, label: Text('Event'), ), ], selected: {_type}, onSelectionChanged: (Set 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: [ 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: [ TextField( controller: _titleController, decoration: const InputDecoration(labelText: 'Title'), ), const SizedBox(height: 10), DropdownButtonFormField( initialValue: _cadence, decoration: const InputDecoration(labelText: 'Cadence'), items: ReminderCadence.values .map( (ReminderCadence cadence) => DropdownMenuItem( 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: [ 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: [ 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'), ), ], ); } }