import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:relationship_saver/features/local/local_repository.dart'; import 'package:relationship_saver/features/share_intake/domain/share_capture_draft_suggester.dart'; import 'package:relationship_saver/features/share_intake/share_capture_models.dart'; Future showShareCaptureReviewSheet( BuildContext context, { required WidgetRef ref, required SharedPayload payload, String? initialPersonId, }) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, showDragHandle: true, builder: (BuildContext context) { return _ShareCaptureReviewSheet( payload: payload, initialPersonId: initialPersonId, ); }, ); } class _ShareCaptureReviewSheet extends ConsumerStatefulWidget { const _ShareCaptureReviewSheet({required this.payload, this.initialPersonId}); final SharedPayload payload; final String? initialPersonId; @override ConsumerState<_ShareCaptureReviewSheet> createState() => _ShareCaptureReviewSheetState(); } class _ShareCaptureReviewSheetState extends ConsumerState<_ShareCaptureReviewSheet> { late final TextEditingController _textController; late final TextEditingController _labelController; late CapturedFactType _type; String? _selectedPersonId; DateTime? _selectedDate; late bool _isSensitive; bool _submitting = false; @override void initState() { super.initState(); final CapturedFactDraft suggestedDraft = ShareCaptureDraftSuggester.suggestForPayload(widget.payload); _selectedPersonId = widget.initialPersonId; _type = suggestedDraft.type; _selectedDate = suggestedDraft.dateValue; _isSensitive = suggestedDraft.isSensitive; _textController = TextEditingController(text: suggestedDraft.text); _labelController = TextEditingController(text: suggestedDraft.label ?? ''); } @override void dispose() { _textController.dispose(); _labelController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final AsyncValue localData = ref.watch( localRepositoryProvider, ); return localData.when( data: (LocalDataState state) { final List people = state.people.toList(growable: false) ..sort(_recentPeopleSort); return Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 8, bottom: MediaQuery.of(context).viewInsets.bottom + 16, ), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Save Shared Capture', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 6), Text( 'Review the incoming text or link, then assign it safely.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.textSecondary, ), ), const SizedBox(height: 14), _PayloadPreview(payload: widget.payload), const SizedBox(height: 14), Text( 'What kind of info is this?', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: CapturedFactType.values .map((CapturedFactType type) { return ChoiceChip( label: Text(_factTypeLabel(type)), selected: _type == type, onSelected: (_) { setState(() { _type = type; }); }, ); }) .toList(growable: false), ), const SizedBox(height: 14), TextField( controller: _textController, minLines: 2, maxLines: 4, decoration: InputDecoration( labelText: _type == CapturedFactType.importantDate ? 'Date note' : 'Captured text', hintText: 'Adjust the note, preference, or idea before saving.', ), ), const SizedBox(height: 10), TextField( controller: _labelController, decoration: InputDecoration( labelText: _type == CapturedFactType.importantDate ? 'Date label' : 'Optional label', ), ), if (_type == CapturedFactType.importantDate) ...[ const SizedBox(height: 10), OutlinedButton.icon( onPressed: _pickDate, icon: const Icon(Icons.event_outlined), label: Text( _selectedDate == null ? 'Choose date' : 'Date: ${_dateLabel(_selectedDate!)}', ), ), ], const SizedBox(height: 12), SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, title: const Text('Mark as sensitive'), subtitle: const Text( 'Useful for future privacy controls and review.', ), value: _isSensitive, onChanged: (bool value) { setState(() { _isSensitive = value; }); }, ), const SizedBox(height: 8), Text( 'Assign to person', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), if (people.isEmpty) Text( 'No people yet. Create one or save this to the inbox.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.textSecondary, ), ) else Wrap( spacing: 8, runSpacing: 8, children: people .take(6) .map((PersonProfile person) { return ChoiceChip( label: Text(person.name), selected: _selectedPersonId == person.id, onSelected: (_) { setState(() { _selectedPersonId = person.id; }); }, ); }) .toList(growable: false), ), if (people.isNotEmpty) ...[ const SizedBox(height: 10), DropdownButtonFormField( initialValue: _selectedPersonId, decoration: const InputDecoration( labelText: 'Or choose from all people', ), items: people .map( (PersonProfile person) => DropdownMenuItem( value: person.id, child: Text( '${person.name} • ${person.relationship}', ), ), ) .toList(growable: false), onChanged: (String? value) { setState(() { _selectedPersonId = value; }); }, ), ], const SizedBox(height: 16), Row( children: [ Expanded( child: OutlinedButton( onPressed: _submitting ? null : _saveToInbox, child: const Text('Save To Inbox'), ), ), const SizedBox(width: 8), Expanded( child: FilledButton.tonal( onPressed: _submitting ? null : _createPerson, child: const Text('Create Person'), ), ), ], ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: FilledButton( onPressed: _submitting || _selectedPersonId == null ? null : _saveToSelectedPerson, child: const Text('Save To Person'), ), ), ], ), ), ); }, loading: () => const Padding( padding: EdgeInsets.all(24), child: Center(child: CircularProgressIndicator()), ), error: (_, _) => const Padding( padding: EdgeInsets.all(24), child: Text('Unable to load people for share review.'), ), ); } int _recentPeopleSort(PersonProfile a, PersonProfile b) { final DateTime? left = a.lastInteractedAt ?? a.lastUpdatedAt; final DateTime? right = b.lastInteractedAt ?? b.lastUpdatedAt; if (left == null && right != null) { return 1; } if (left != null && right == null) { return -1; } if (left != null && right != null) { final int byDate = right.compareTo(left); if (byDate != 0) { return byDate; } } return b.affinityScore.compareTo(a.affinityScore); } CapturedFactDraft _buildDraft() { return CapturedFactDraft( type: _type, text: _textController.text.trim(), label: _labelController.text.trim().isEmpty ? null : _labelController.text.trim(), dateValue: _selectedDate, isSensitive: _isSensitive, needsReview: false, ); } Future _saveToSelectedPerson() async { final String? personId = _selectedPersonId; if (personId == null) { return; } await _runAction(() async { final SharedMessageIngestResult result = await ref .read(localRepositoryProvider.notifier) .captureSharedPayloadToExistingProfile( payload: widget.payload, profileId: personId, draft: _buildDraft(), resolvedAutomatically: false, ); if (!mounted) { return; } Navigator.of(context).pop(result); }); } Future _saveToInbox() async { await _runAction(() async { final SharedMessageIngestResult result = await ref .read(localRepositoryProvider.notifier) .saveSharedPayloadToInbox( payload: widget.payload, draft: _buildDraft(), targetPersonId: _selectedPersonId, ); if (!mounted) { return; } Navigator.of(context).pop(result); }); } Future _createPerson() async { final _CreatePersonFromShareDraft? draft = await showDialog<_CreatePersonFromShareDraft>( context: context, builder: (BuildContext context) => _CreatePersonFromShareDialog( initialName: widget.payload.sourceDisplayName ?? widget.payload.rawText.split(RegExp(r'\s+')).take(2).join(' '), ), ); if (draft == null) { return; } await _runAction(() async { final SharedMessageIngestResult result = await ref .read(localRepositoryProvider.notifier) .captureSharedPayloadByCreatingProfile( payload: widget.payload, name: draft.name, relationship: draft.relationship, aliases: draft.aliases, draft: _buildDraft(), ); if (!mounted) { return; } Navigator.of(context).pop(result); }); } Future _pickDate() async { final DateTime now = DateTime.now(); final DateTime? date = await showDatePicker( context: context, firstDate: DateTime(now.year - 5), lastDate: DateTime(now.year + 10), initialDate: _selectedDate ?? now, ); if (date == null) { return; } setState(() { _selectedDate = date; }); } Future _runAction(Future Function() action) async { setState(() { _submitting = true; }); try { await action(); } finally { if (mounted) { setState(() { _submitting = false; }); } } } } String _factTypeLabel(CapturedFactType type) { return switch (type) { CapturedFactType.note => 'Note', CapturedFactType.like => 'Like', CapturedFactType.dislike => 'Dislike', CapturedFactType.importantDate => 'Date', CapturedFactType.giftIdea => 'Gift', CapturedFactType.placeIdea => 'Place', CapturedFactType.activityIdea => 'Activity', CapturedFactType.misc => 'Misc', }; } String _dateLabel(DateTime value) { return '${value.year}-${value.month.toString().padLeft(2, '0')}-${value.day.toString().padLeft(2, '0')}'; } class _PayloadPreview extends StatelessWidget { const _PayloadPreview({required this.payload}); final SharedPayload payload; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5EDF2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( payload.sourceApp.toUpperCase(), style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary), ), if (payload.sourceDisplayName != null) ...[ const SizedBox(height: 4), Text( 'Sender: ${payload.sourceDisplayName}', style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary), ), ], if (payload.rawText.trim().isNotEmpty) ...[ const SizedBox(height: 8), Text(payload.rawText.trim()), ], if (payload.url != null) ...[ const SizedBox(height: 8), SelectableText( payload.url!, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.primary, decoration: TextDecoration.underline, ), ), ], ], ), ); } } class _CreatePersonFromShareDraft { const _CreatePersonFromShareDraft({ required this.name, required this.relationship, required this.aliases, }); final String name; final String relationship; final List aliases; } class _CreatePersonFromShareDialog extends StatefulWidget { const _CreatePersonFromShareDialog({required this.initialName}); final String initialName; @override State<_CreatePersonFromShareDialog> createState() => _CreatePersonFromShareDialogState(); } class _CreatePersonFromShareDialogState extends State<_CreatePersonFromShareDialog> { late final TextEditingController _nameController; final TextEditingController _relationshipController = TextEditingController( text: 'Contact', ); final TextEditingController _aliasesController = TextEditingController(); @override void initState() { super.initState(); _nameController = TextEditingController(text: widget.initialName); } @override void dispose() { _nameController.dispose(); _relationshipController.dispose(); _aliasesController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Create Person'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _nameController, decoration: const InputDecoration(labelText: 'Name'), ), const SizedBox(height: 10), TextField( controller: _relationshipController, decoration: const InputDecoration(labelText: 'Relationship'), ), const SizedBox(height: 10), TextField( controller: _aliasesController, decoration: const InputDecoration( labelText: 'Aliases (comma separated)', ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { final String name = _nameController.text.trim(); final String relationship = _relationshipController.text.trim(); if (name.isEmpty || relationship.isEmpty) { return; } Navigator.of(context).pop( _CreatePersonFromShareDraft( name: name, relationship: relationship, aliases: _aliasesController.text .split(',') .map((String value) => value.trim()) .where((String value) => value.isNotEmpty) .toList(growable: false), ), ); }, child: const Text('Create'), ), ], ); } }