import 'dart:math' as math; 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 DashboardView extends ConsumerWidget { const DashboardView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final AsyncValue localData = ref.watch( localRepositoryProvider, ); return localData.when( data: (LocalDataState data) { final DashboardSummary summary = data.summary(); final List tasks = data.tasks; return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool compact = constraints.maxWidth < 720; return SingleChildScrollView( padding: EdgeInsets.fromLTRB( compact ? 16 : 28, compact ? 16 : 24, compact ? 16 : 28, compact ? 24 : 36, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Today', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 8), Text( 'See the relationship graph first, then execute the highest-leverage next moves.', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), ), const SizedBox(height: 18), _RelationshipGraphCard(people: data.people, compact: compact), const SizedBox(height: 20), Wrap( spacing: 12, runSpacing: 12, children: [ _StatCard( label: 'Active People', value: '${summary.activePeople}', accent: const Color(0xFF16B8CA), compact: compact, ), _StatCard( label: 'Upcoming Plans', value: '${summary.upcomingPlans}', accent: const Color(0xFF2B7FFF), compact: compact, ), _StatCard( label: 'Pending Ideas', value: '${summary.pendingIdeas}', accent: const Color(0xFFFFA447), compact: compact, ), _StatCard( label: 'Weekly Rhythm', value: '${summary.weeklyConsistency}%', accent: const Color(0xFF30B66A), compact: compact, ), ], ), const SizedBox(height: 24), FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Next Moves', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 14), if (tasks.isEmpty) Text( 'No tasks yet. Add moments, ideas, or reminders to generate follow-through actions.', style: Theme.of(context).textTheme.bodyLarge ?.copyWith(color: AppTheme.textSecondary), ) else ...tasks.map( (DashboardTask task) => _TaskRow( task: task, compact: compact, onToggleDone: () { ref .read(localRepositoryProvider.notifier) .toggleTaskDone(task.id); }, ), ), ], ), ), ], ), ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (Object error, StackTrace stackTrace) { return Center( child: Text( 'Could not load local data.', style: Theme.of(context).textTheme.bodyLarge, ), ); }, ); } } class _RelationshipGraphCard extends StatelessWidget { const _RelationshipGraphCard({required this.people, required this.compact}); final List people; final bool compact; @override Widget build(BuildContext context) { return FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( 'Relationship Graph', style: Theme.of(context).textTheme.titleLarge, ), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: const Color(0xFFEAF7FB), borderRadius: BorderRadius.circular(99), ), child: Text( '${people.length} nodes', style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.primary), ), ), ], ), const SizedBox(height: 12), Container( height: compact ? 330 : 410, decoration: BoxDecoration( gradient: LinearGradient( colors: [ const Color(0xFFEAF3F8), const Color(0xFFF7FAFC), Colors.white.withValues(alpha: 0.9), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(24), border: Border.all(color: Colors.white.withValues(alpha: 0.8)), ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { if (people.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( 'No people yet. Add your first relationship to start the graph.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge ?.copyWith(color: AppTheme.textSecondary), ), ), ); } final _GraphLayout layout = _GraphLayout.build( people: people, size: constraints.biggest, compact: compact, ); return Stack( children: [ Positioned.fill( child: CustomPaint( painter: _RelationshipEdgePainter( center: layout.center, edges: layout.edges, ), ), ), ...layout.nodes.map( (_GraphNode node) => _GraphBubble(node: node, compact: compact), ), _CenterNode(center: layout.center, compact: compact), if (layout.hiddenCount > 0) Positioned( right: 12, top: 12, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(99), ), child: Text( '+${layout.hiddenCount} more', style: Theme.of(context).textTheme.labelLarge ?.copyWith(color: AppTheme.textSecondary), ), ), ), ], ); }, ), ), ), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: _collectLegend(people) .map( (_RelationshipVisual visual) => _RelationshipLegendPill(visual: visual), ) .toList(growable: false), ), ], ), ); } List<_RelationshipVisual> _collectLegend(List source) { final Set seen = {}; final List<_RelationshipVisual> result = <_RelationshipVisual>[]; for (final PersonProfile person in source) { final _RelationshipVisual visual = _relationshipVisualFor( person.relationship, ); if (seen.add(visual.label)) { result.add(visual); } if (result.length >= 7) { break; } } if (result.isEmpty) { result.add(_otherVisual); } return result; } } class _GraphLayout { const _GraphLayout({ required this.center, required this.nodes, required this.edges, required this.hiddenCount, }); final Offset center; final List<_GraphNode> nodes; final List<_GraphEdge> edges; final int hiddenCount; static _GraphLayout build({ required List people, required Size size, required bool compact, }) { final int maxNodes = compact ? 10 : 16; final List visiblePeople = people .take(maxNodes) .toList(growable: false); final int hiddenCount = people.length - visiblePeople.length; final Offset center = Offset(size.width / 2, size.height / 2); if (visiblePeople.isEmpty) { return _GraphLayout( center: center, nodes: const <_GraphNode>[], edges: const <_GraphEdge>[], hiddenCount: hiddenCount, ); } final double minSide = math.min(size.width, size.height); final int total = visiblePeople.length; final int innerCount = total <= 8 ? total : 8; final int outerCount = total - innerCount; final double innerRadius = minSide * (compact ? 0.28 : 0.31); final double outerRadius = minSide * (compact ? 0.39 : 0.43); final List<_GraphNode> nodes = <_GraphNode>[]; for (int i = 0; i < total; i += 1) { final bool inOuterRing = i >= innerCount; final int ringCount = inOuterRing ? outerCount : innerCount; final int ringIndex = inOuterRing ? i - innerCount : i; final double radius = inOuterRing ? outerRadius : innerRadius; final double phaseOffset = inOuterRing ? math.pi / ringCount : 0; final double angle = (-math.pi / 2) + phaseOffset + ((2 * math.pi * ringIndex) / ringCount); final Offset position = Offset( center.dx + radius * math.cos(angle), center.dy + radius * math.sin(angle), ); final PersonProfile person = visiblePeople[i]; final _RelationshipVisual visual = _relationshipVisualFor( person.relationship, ); nodes.add(_GraphNode(person: person, visual: visual, position: position)); } final List<_GraphEdge> edges = <_GraphEdge>[]; for (final _GraphNode node in nodes) { edges.add( _GraphEdge( start: center, end: node.position, color: node.visual.color.withValues(alpha: 0.46), width: compact ? 1.7 : 2.0, ), ); } if (nodes.length > 2) { for (int i = 0; i < nodes.length; i += 1) { final _GraphNode current = nodes[i]; final _GraphNode next = nodes[(i + 1) % nodes.length]; final Color color = Color.lerp(current.visual.color, next.visual.color, 0.5) ?? AppTheme.accent; edges.add( _GraphEdge( start: current.position, end: next.position, color: color.withValues(alpha: 0.2), width: 1.1, ), ); if (nodes.length >= 7 && i.isEven) { final _GraphNode jump = nodes[(i + 3) % nodes.length]; edges.add( _GraphEdge( start: current.position, end: jump.position, color: current.visual.color.withValues(alpha: 0.1), width: 1.0, ), ); } } } return _GraphLayout( center: center, nodes: nodes, edges: edges, hiddenCount: hiddenCount, ); } } class _GraphNode { const _GraphNode({ required this.person, required this.visual, required this.position, }); final PersonProfile person; final _RelationshipVisual visual; final Offset position; } class _GraphEdge { const _GraphEdge({ required this.start, required this.end, required this.color, required this.width, }); final Offset start; final Offset end; final Color color; final double width; } class _RelationshipEdgePainter extends CustomPainter { const _RelationshipEdgePainter({required this.center, required this.edges}); final Offset center; final List<_GraphEdge> edges; @override void paint(Canvas canvas, Size size) { final Paint centerGlow = Paint() ..shader = RadialGradient( colors: [ AppTheme.accent.withValues(alpha: 0.15), Colors.transparent, ], ).createShader( Rect.fromCircle( center: center, radius: math.min(size.width, size.height) * 0.36, ), ); canvas.drawCircle( center, math.min(size.width, size.height) * 0.36, centerGlow, ); for (final _GraphEdge edge in edges) { final Paint paint = Paint() ..color = edge.color ..strokeWidth = edge.width ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke; canvas.drawLine(edge.start, edge.end, paint); } } @override bool shouldRepaint(covariant _RelationshipEdgePainter oldDelegate) { return oldDelegate.edges != edges || oldDelegate.center != center; } } class _GraphBubble extends StatelessWidget { const _GraphBubble({required this.node, required this.compact}); final _GraphNode node; final bool compact; @override Widget build(BuildContext context) { final double diameter = compact ? 52 : 62; final double labelWidth = compact ? 66 : 86; return Positioned( left: node.position.dx - (diameter / 2), top: node.position.dy - (diameter / 2), child: Tooltip( message: '${node.person.name} • ${node.visual.label}', child: SizedBox( width: labelWidth, child: Column( mainAxisSize: MainAxisSize.min, children: [ Stack( clipBehavior: Clip.none, children: [ Container( width: diameter, height: diameter, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [ _shiftLightness(node.visual.color, 0.16), _shiftLightness(node.visual.color, -0.12), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: node.visual.color.withValues(alpha: 0.24), blurRadius: 14, offset: const Offset(0, 8), ), ], ), alignment: Alignment.center, child: Text( _initials(node.person.name), style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, ), ), ), Positioned( right: -2, top: -2, child: Container( width: compact ? 20 : 22, height: compact ? 20 : 22, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all( color: node.visual.color.withValues(alpha: 0.8), ), ), child: Icon( node.visual.icon, size: compact ? 12 : 13, color: node.visual.color, ), ), ), ], ), if (!compact) ...[ const SizedBox(height: 5), Text( _shortName(node.person.name), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelMedium?.copyWith( color: AppTheme.textSecondary, ), ), ], ], ), ), ), ); } } class _CenterNode extends StatelessWidget { const _CenterNode({required this.center, required this.compact}); final Offset center; final bool compact; @override Widget build(BuildContext context) { final double diameter = compact ? 74 : 86; return Positioned( left: center.dx - (diameter / 2), top: center.dy - (diameter / 2), child: Container( width: diameter, height: diameter, decoration: BoxDecoration( shape: BoxShape.circle, gradient: const LinearGradient( colors: [Color(0xFF1AB6C8), Color(0xFF286DE0)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: const Color(0xFF1AB6C8).withValues(alpha: 0.26), blurRadius: 20, offset: const Offset(0, 10), ), ], ), alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.favorite_rounded, color: Colors.white, size: 18), const SizedBox(height: 2), Text( 'YOU', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, letterSpacing: 0.4, ), ), ], ), ), ); } } class _RelationshipLegendPill extends StatelessWidget { const _RelationshipLegendPill({required this.visual}); final _RelationshipVisual visual; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: visual.color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(99), border: Border.all(color: visual.color.withValues(alpha: 0.18)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(visual.icon, size: 14, color: visual.color), const SizedBox(width: 6), Text( visual.label, style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary), ), ], ), ); } } class _StatCard extends StatelessWidget { const _StatCard({ required this.label, required this.value, required this.accent, required this.compact, }); final String label; final String value; final Color accent; final bool compact; @override Widget build(BuildContext context) { return FrostedCard( padding: const EdgeInsets.all(16), child: SizedBox( width: compact ? double.infinity : 188, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 8, width: 40, decoration: BoxDecoration( color: accent, borderRadius: BorderRadius.circular(99), ), ), const SizedBox(height: 16), Text( value, style: Theme.of( context, ).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 4), Text( label, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), ), ], ), ), ); } } class _TaskRow extends StatelessWidget { const _TaskRow({ required this.task, required this.onToggleDone, required this.compact, }); final DashboardTask task; final VoidCallback onToggleDone; final bool compact; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFF5FAFB), borderRadius: BorderRadius.circular(16), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( onTap: onToggleDone, borderRadius: BorderRadius.circular(20), child: Icon( task.done ? Icons.check_circle_rounded : Icons.radio_button_unchecked_rounded, color: task.done ? const Color(0xFF1D9C66) : const Color(0xFF1AB6C8), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.title, style: Theme.of(context).textTheme.titleMedium?.copyWith( decoration: task.done ? TextDecoration.lineThrough : null, ), ), const SizedBox(height: 4), Text( task.description, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.textSecondary, ), ), ], ), ), if (!compact) ...[ const SizedBox(width: 12), Text( _shortDate(task.when), style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary), ), ], ], ), ), ); } } class _RelationshipVisual { const _RelationshipVisual({ required this.label, required this.icon, required this.color, }); final String label; final IconData icon; final Color color; } const _RelationshipVisual _spouseVisual = _RelationshipVisual( label: 'Spouse', icon: Icons.favorite_rounded, color: Color(0xFFE0557B), ); const _RelationshipVisual _fianceVisual = _RelationshipVisual( label: 'Fiance', icon: Icons.diamond_rounded, color: Color(0xFFCB6CF2), ); const _RelationshipVisual _partnerVisual = _RelationshipVisual( label: 'Partner', icon: Icons.favorite_border_rounded, color: Color(0xFFE67A5A), ); const _RelationshipVisual _bestieVisual = _RelationshipVisual( label: 'Bestie', icon: Icons.emoji_emotions_rounded, color: Color(0xFF8E6BFF), ); const _RelationshipVisual _friendVisual = _RelationshipVisual( label: 'Friend', icon: Icons.groups_rounded, color: Color(0xFF3F8CFF), ); const _RelationshipVisual _familyVisual = _RelationshipVisual( label: 'Family', icon: Icons.family_restroom_rounded, color: Color(0xFF2FB67D), ); const _RelationshipVisual _sisterVisual = _RelationshipVisual( label: 'Sister', icon: Icons.female_rounded, color: Color(0xFFFF6EA9), ); const _RelationshipVisual _brotherVisual = _RelationshipVisual( label: 'Brother', icon: Icons.male_rounded, color: Color(0xFF4A7DFF), ); const _RelationshipVisual _otherVisual = _RelationshipVisual( label: 'Connection', icon: Icons.hub_rounded, color: Color(0xFF5F7280), ); _RelationshipVisual _relationshipVisualFor(String relationship) { final String value = relationship.toLowerCase(); if (_containsAny(value, ['spouse', 'wife', 'husband'])) { return _spouseVisual; } if (_containsAny(value, ['fiance', 'engaged'])) { return _fianceVisual; } if (_containsAny(value, [ 'girlfriend', 'boyfriend', 'partner', 'gf', 'bf', ])) { return _partnerVisual; } if (_containsAny(value, ['bestie', 'best friend', 'bff'])) { return _bestieVisual; } if (_containsAny(value, ['sister', 'sis'])) { return _sisterVisual; } if (_containsAny(value, ['brother', 'bro'])) { return _brotherVisual; } if (_containsAny(value, [ 'family', 'mother', 'father', 'mom', 'dad', 'parent', 'aunt', 'uncle', 'cousin', 'grand', ])) { return _familyVisual; } if (_containsAny(value, ['friend', 'pal', 'buddy', 'mate'])) { return _friendVisual; } return _otherVisual; } bool _containsAny(String value, List needles) { for (final String needle in needles) { if (value.contains(needle)) { return true; } } return false; } Color _shiftLightness(Color color, double delta) { final HSLColor hsl = HSLColor.fromColor(color); final double next = (hsl.lightness + delta).clamp(0.0, 1.0); return hsl.withLightness(next).toColor(); } String _initials(String name) { final List parts = name .split(' ') .where((String part) => part.isNotEmpty) .toList(growable: false); if (parts.isEmpty) { return '?'; } if (parts.length == 1) { return parts.first .substring(0, math.min(2, parts.first.length)) .toUpperCase(); } return '${parts.first[0]}${parts[1][0]}'.toUpperCase(); } String _shortName(String name) { final List parts = name .split(' ') .where((String part) => part.isNotEmpty) .toList(growable: false); if (parts.isEmpty) { return 'Unknown'; } return parts.first; } String _shortDate(DateTime value) { const List weekdays = [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', ]; final String weekday = weekdays[value.weekday - 1]; final String day = value.day.toString().padLeft(2, '0'); return '$weekday $day'; }