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/shared/frosted_card.dart'; class SelectedPersonIdNotifier extends Notifier { @override String? build() => null; void select(String id) { state = id; } void clear() { state = null; } } final NotifierProvider selectedPersonIdProvider = NotifierProvider( SelectedPersonIdNotifier.new, ); class PeopleView extends ConsumerWidget { const PeopleView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final AsyncValue localData = ref.watch( localRepositoryProvider, ); return localData.when( data: (LocalDataState data) { final List people = data.people; if (people.isEmpty) { return _EmptyPeopleView(onAdd: () => _handleAddPerson(context, ref)); } final String selectedId = ref.watch(selectedPersonIdProvider) ?? people.first.id; final PersonProfile selected = people.firstWhere( (PersonProfile person) => person.id == selectedId, orElse: () => people.first, ); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool isCompact = constraints.maxWidth < 860; if (isCompact) { return _buildCompactLayout(context, ref, people, selected); } return _buildWideLayout(context, ref, people, selected); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (Object error, StackTrace stackTrace) { return const Center(child: Text('Unable to load people')); }, ); } Future _handleAddPerson(BuildContext context, WidgetRef ref) async { final _PersonDraft? draft = await showDialog<_PersonDraft>( context: context, builder: (BuildContext context) => const _PersonEditorDialog(title: 'Add Person'), ); if (draft == null) { return; } await ref .read(localRepositoryProvider.notifier) .addPerson( name: draft.name, relationship: draft.relationship, notes: draft.notes, tags: draft.tags, ); } Future _handleEditPerson( BuildContext context, WidgetRef ref, PersonProfile person, ) async { final _PersonDraft? draft = await showDialog<_PersonDraft>( context: context, builder: (BuildContext context) => _PersonEditorDialog( title: 'Edit Person', initial: _PersonDraft( name: person.name, relationship: person.relationship, notes: person.notes, tags: person.tags, ), ), ); if (draft == null) { return; } await ref .read(localRepositoryProvider.notifier) .updatePerson( person.copyWith( name: draft.name, relationship: draft.relationship, notes: draft.notes, tags: draft.tags, ), ); } Future _handleDeletePerson( BuildContext context, WidgetRef ref, String personId, ) async { final bool? confirmed = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Delete person?'), content: const Text( 'This will remove the person and related moments from local storage.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Delete'), ), ], ); }, ); if (confirmed != true) { return; } await ref.read(localRepositoryProvider.notifier).deletePerson(personId); ref.read(selectedPersonIdProvider.notifier).clear(); } Widget _buildWideLayout( BuildContext context, WidgetRef ref, List people, PersonProfile selected, ) { return Padding( padding: const EdgeInsets.fromLTRB(28, 24, 28, 28), child: Row( children: [ Expanded( flex: 5, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _PeopleHeader( onAdd: () => _handleAddPerson(context, ref), compact: false, ), const SizedBox(height: 20), Expanded( child: ListView.separated( itemCount: people.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (BuildContext context, int index) { final PersonProfile person = people[index]; return _PersonListItem( person: person, selectedId: selected.id, onSelect: () { ref .read(selectedPersonIdProvider.notifier) .select(person.id); }, ); }, ), ), ], ), ), const SizedBox(width: 18), Expanded( flex: 6, child: FrostedCard( child: _PersonDetail( person: selected, onEdit: () => _handleEditPerson(context, ref, selected), onDelete: () => _handleDeletePerson(context, ref, selected.id), ), ), ), ], ), ); } Widget _buildCompactLayout( BuildContext context, WidgetRef ref, List people, PersonProfile selected, ) { return Padding( padding: const EdgeInsets.fromLTRB(18, 16, 18, 20), child: ListView( children: [ _PeopleHeader( onAdd: () => _handleAddPerson(context, ref), compact: true, ), const SizedBox(height: 14), ...people.map( (PersonProfile person) => Padding( padding: const EdgeInsets.only(bottom: 10), child: _PersonListItem( person: person, selectedId: selected.id, onSelect: () { ref.read(selectedPersonIdProvider.notifier).select(person.id); }, ), ), ), const SizedBox(height: 8), FrostedCard( child: _PersonDetail( person: selected, compact: true, onEdit: () => _handleEditPerson(context, ref, selected), onDelete: () => _handleDeletePerson(context, ref, selected.id), ), ), ], ), ); } } class _EmptyPeopleView extends StatelessWidget { const _EmptyPeopleView({required this.onAdd}); final VoidCallback onAdd; @override Widget build(BuildContext context) { return Center( child: FrostedCard( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'No people yet', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( 'Add the first relationship profile to start tracking moments.', style: Theme.of( context, ).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), textAlign: TextAlign.center, ), const SizedBox(height: 12), FilledButton.icon( onPressed: onAdd, icon: const Icon(Icons.person_add_alt_1_rounded), label: const Text('Add Person'), ), ], ), ), ); } } class _PersonDetail extends StatelessWidget { const _PersonDetail({ required this.person, required this.onEdit, required this.onDelete, this.compact = false, }); final PersonProfile person; final VoidCallback onEdit; final VoidCallback onDelete; final bool compact; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (compact) Column( children: [ Row( children: [ _AvatarSeed(name: person.name, size: 56), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( person.name, style: Theme.of(context).textTheme.titleLarge, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( person.relationship, style: Theme.of(context).textTheme.bodyLarge ?.copyWith(color: AppTheme.textSecondary), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( onPressed: onEdit, icon: const Icon(Icons.edit_rounded), tooltip: 'Edit person', ), IconButton( onPressed: onDelete, icon: const Icon(Icons.delete_outline_rounded), tooltip: 'Delete person', ), ], ), ], ) else Row( children: [ _AvatarSeed(name: person.name, size: 64), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( person.name, style: Theme.of(context).textTheme.titleLarge, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( person.relationship, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), IconButton( onPressed: onEdit, icon: const Icon(Icons.edit_rounded), tooltip: 'Edit person', ), IconButton( onPressed: onDelete, icon: const Icon(Icons.delete_outline_rounded), tooltip: 'Delete person', ), ], ), SizedBox(height: compact ? 14 : 22), Text('Next moment', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), _DetailTag( label: '${person.nextMoment.year}-${person.nextMoment.month.toString().padLeft(2, '0')}-${person.nextMoment.day.toString().padLeft(2, '0')} ${person.nextMoment.hour.toString().padLeft(2, '0')}:${person.nextMoment.minute.toString().padLeft(2, '0')}', ), const SizedBox(height: 18), Text('Preferences', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: person.tags .map((String tag) => _DetailTag(label: tag)) .toList(growable: false), ), const SizedBox(height: 18), Text('Notes', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), Text( person.notes, style: Theme.of( context, ).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), ), ], ); } } class _PeopleHeader extends StatelessWidget { const _PeopleHeader({required this.onAdd, required this.compact}); final VoidCallback onAdd; final bool compact; @override Widget build(BuildContext context) { final TextTheme textTheme = Theme.of(context).textTheme; if (compact) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('People', style: textTheme.headlineMedium), const SizedBox(height: 6), Text( 'Track what matters to each relationship and follow through.', style: textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 12), FilledButton.icon( onPressed: onAdd, icon: const Icon(Icons.person_add_alt_1_rounded), label: const Text('Add person'), ), ], ); } return Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('People', style: textTheme.headlineMedium), const SizedBox(height: 6), Text( 'Track what matters to each relationship and follow through.', style: textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), ), ], ), ), FilledButton.icon( onPressed: onAdd, icon: const Icon(Icons.person_add_alt_1_rounded), label: const Text('Add'), ), ], ); } } class _PersonListItem extends StatelessWidget { const _PersonListItem({ required this.person, required this.selectedId, required this.onSelect, }); final PersonProfile person; final String selectedId; final VoidCallback onSelect; @override Widget build(BuildContext context) { final bool isSelected = person.id == selectedId; return InkWell( onTap: onSelect, borderRadius: BorderRadius.circular(20), child: FrostedCard( padding: const EdgeInsets.all(16), child: Row( children: [ _AvatarSeed(name: person.name), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( person.name, style: Theme.of(context).textTheme.titleMedium, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( person.relationship, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.textSecondary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 8), _ScorePill(score: person.affinityScore), if (isSelected) const Padding( padding: EdgeInsets.only(left: 10), child: Icon(Icons.check_circle, color: Color(0xFF1AB6C8)), ), ], ), ), ); } } class _PersonEditorDialog extends StatefulWidget { const _PersonEditorDialog({required this.title, this.initial}); final String title; final _PersonDraft? initial; @override State<_PersonEditorDialog> createState() => _PersonEditorDialogState(); } class _PersonEditorDialogState extends State<_PersonEditorDialog> { late final TextEditingController _nameController; late final TextEditingController _relationshipController; late final TextEditingController _tagsController; late final TextEditingController _notesController; @override void initState() { super.initState(); _nameController = TextEditingController(text: widget.initial?.name ?? ''); _relationshipController = TextEditingController( text: widget.initial?.relationship ?? '', ); _tagsController = TextEditingController( text: widget.initial?.tags.join(', ') ?? '', ); _notesController = TextEditingController(text: widget.initial?.notes ?? ''); } @override void dispose() { _nameController.dispose(); _relationshipController.dispose(); _tagsController.dispose(); _notesController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: Text(widget.title), content: SingleChildScrollView( child: SizedBox( width: 420, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _nameController, decoration: const InputDecoration(labelText: 'Name'), ), TextField( controller: _relationshipController, decoration: const InputDecoration(labelText: 'Relationship'), ), TextField( controller: _tagsController, decoration: const InputDecoration( labelText: 'Tags (comma separated)', ), ), TextField( controller: _notesController, maxLines: 3, decoration: const InputDecoration(labelText: 'Notes'), ), ], ), ), ), 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; } final _PersonDraft draft = _PersonDraft( name: name, relationship: relationship, 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 _PersonDraft { const _PersonDraft({ required this.name, required this.relationship, required this.notes, required this.tags, }); final String name; final String relationship; final String notes; final List tags; } class _DetailTag extends StatelessWidget { const _DetailTag({required this.label}); final String label; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFF2F8FA), borderRadius: BorderRadius.circular(99), ), child: Text(label, style: Theme.of(context).textTheme.bodyMedium), ); } } class _AvatarSeed extends StatelessWidget { const _AvatarSeed({required this.name, this.size = 48}); final String name; final double size; @override Widget build(BuildContext context) { final String initials = name .split(' ') .where((String part) => part.isNotEmpty) .take(2) .map((String part) => part[0].toUpperCase()) .join(); return Container( width: size, height: size, decoration: BoxDecoration( borderRadius: BorderRadius.circular(size / 2), gradient: const LinearGradient( colors: [Color(0xFF1AB6C8), Color(0xFF2274E0)], ), ), alignment: Alignment.center, child: Text( initials, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, ), ), ); } } class _ScorePill extends StatelessWidget { const _ScorePill({required this.score}); final int score; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFEEF7F3), borderRadius: BorderRadius.circular(99), ), child: Text( '$score%', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: const Color(0xFF1D9C66), fontWeight: FontWeight.w700, ), ), ); } }