import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:tatlock_ui/features/front_hall/data/models/environment_model.dart'; /// Sun position widget displaying sunrise/sunset with animated arc. /// /// Shows a semicircle arc representing the day, with sun/moon icon /// moving along based on current time. Uses color gradients: /// - Blue tones for night /// - Orange tones for sunrise/sunset /// - Yellow tones for daytime class SunPositionWidget extends StatelessWidget { const SunPositionWidget({ super.key, this.sunTimes, this.compact = false, }); /// Sun times data to display. final SunTimesData? sunTimes; /// Whether to use compact layout. final bool compact; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; if (compact) { return _buildCompact(context, colorScheme); } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Header Row( children: [ Icon( _isDaytime ? Icons.wb_sunny : Icons.nightlight_round, size: 20, color: colorScheme.onSurfaceVariant, ), const SizedBox(width: 8), Text( 'Sun Position', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ), const SizedBox(height: 16), // Arc visualization SizedBox( height: 120, child: CustomPaint( size: const Size(double.infinity, 120), painter: _SunArcPainter( sunTimes: sunTimes, isDark: colorScheme.brightness == Brightness.dark, ), ), ), const SizedBox(height: 12), // Sunrise/Sunset times Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _TimeDisplay( icon: Icons.wb_twilight, label: 'Sunrise', time: sunTimes?.sunrise, iconColor: Colors.orange, ), if (sunTimes?.daylightMinutes != null) _DaylightDisplay(minutes: sunTimes!.daylightMinutes!), _TimeDisplay( icon: Icons.nights_stay, label: 'Sunset', time: sunTimes?.sunset, iconColor: Colors.deepOrange, ), ], ), ], ), ), ); } Widget _buildCompact(BuildContext context, ColorScheme colorScheme) { return Card( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _isDaytime ? Icons.wb_sunny : Icons.nightlight_round, size: 24, color: _isDaytime ? Colors.amber : Colors.indigo, ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( _formatTime(sunTimes?.sunrise), style: Theme.of(context).textTheme.labelSmall, ), Text( _formatTime(sunTimes?.sunset), style: Theme.of(context).textTheme.labelSmall, ), ], ), ], ), ), ); } bool get _isDaytime { if (sunTimes?.sunrise == null || sunTimes?.sunset == null) return true; final now = DateTime.now(); return now.isAfter(sunTimes!.sunrise!) && now.isBefore(sunTimes!.sunset!); } String _formatTime(DateTime? time) { if (time == null) return '--:--'; return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; } } class _TimeDisplay extends StatelessWidget { const _TimeDisplay({ required this.icon, required this.label, required this.time, this.iconColor, }); final IconData icon; final String label; final DateTime? time; final Color? iconColor; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Column( children: [ Icon( icon, size: 20, color: iconColor ?? colorScheme.primary, ), const SizedBox(height: 4), Text( _formatTime(time), style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), Text( label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ); } String _formatTime(DateTime? time) { if (time == null) return '--:--'; return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; } } class _DaylightDisplay extends StatelessWidget { const _DaylightDisplay({required this.minutes}); final int minutes; @override Widget build(BuildContext context) { final hours = minutes ~/ 60; final mins = minutes % 60; return Column( children: [ Icon( Icons.access_time, size: 16, color: Theme.of(context).colorScheme.outline, ), const SizedBox(height: 4), Text( '${hours}h ${mins}m', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w500, ), ), Text( 'Daylight', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ); } } /// Custom painter for the sun arc visualization. class _SunArcPainter extends CustomPainter { _SunArcPainter({ this.sunTimes, this.isDark = false, }); final SunTimesData? sunTimes; final bool isDark; // Color palette static const _nightBlue = Color(0xFF1a237e); static const _dawnOrange = Color(0xFFff6f00); static const _sunriseOrange = Color(0xFFffa000); static const _dayYellow = Color(0xFFffc107); static const _dayGold = Color(0xFFffb300); static const _duskOrange = Color(0xFFef6c00); static const _nightIndigo = Color(0xFF283593); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height - 10); final radius = math.min(size.width / 2 - 20, size.height - 30); // Draw horizon line final horizonPaint = Paint() ..color = isDark ? Colors.white24 : Colors.black12 ..strokeWidth = 1; canvas.drawLine( Offset(center.dx - radius - 10, center.dy), Offset(center.dx + radius + 10, center.dy), horizonPaint, ); // Create gradient for arc background final arcRect = Rect.fromCenter( center: center, width: radius * 2, height: radius * 2, ); // Draw arc background (night portion below horizon implied) final arcBgPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 8 ..strokeCap = StrokeCap.round; // Create gradient that transitions through time of day colors arcBgPaint.shader = SweepGradient( center: Alignment.bottomCenter, startAngle: math.pi, endAngle: 2 * math.pi, colors: [ _nightBlue.withValues(alpha: 0.3), _dawnOrange.withValues(alpha: 0.5), _sunriseOrange.withValues(alpha: 0.6), _dayYellow.withValues(alpha: 0.7), _dayGold.withValues(alpha: 0.7), _dayYellow.withValues(alpha: 0.6), _duskOrange.withValues(alpha: 0.5), _nightIndigo.withValues(alpha: 0.3), ], stops: const [0.0, 0.1, 0.2, 0.4, 0.5, 0.6, 0.8, 1.0], transform: const GradientRotation(-math.pi / 2), ).createShader(arcRect); // Draw the arc (semicircle from left to right) canvas.drawArc( arcRect, math.pi, // Start at left (180 degrees) math.pi, // Sweep 180 degrees (semicircle) false, arcBgPaint, ); // Calculate sun position final sunPosition = _calculateSunPosition(); // Draw sun/moon indicator final angle = math.pi + (sunPosition * math.pi); // Map 0-1 to pi-2pi final sunX = center.dx + radius * math.cos(angle); final sunY = center.dy + radius * math.sin(angle); // Determine if day or night for icon/color final isDaytime = sunPosition > 0 && sunPosition < 1; final isNearHorizon = sunPosition < 0.15 || sunPosition > 0.85; // Sun/moon glow if (isDaytime) { final glowColor = isNearHorizon ? _dawnOrange : _dayYellow; final glowPaint = Paint() ..shader = RadialGradient( colors: [ glowColor.withValues(alpha: 0.6), glowColor.withValues(alpha: 0.0), ], ).createShader( Rect.fromCircle(center: Offset(sunX, sunY), radius: 24), ); canvas.drawCircle(Offset(sunX, sunY), 24, glowPaint); } // Sun/moon circle final sunPaint = Paint() ..style = PaintingStyle.fill ..color = isDaytime ? (isNearHorizon ? _sunriseOrange : _dayYellow) : _nightIndigo; canvas.drawCircle(Offset(sunX, sunY), 12, sunPaint); // Sun rays or moon craters if (isDaytime) { final rayPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2 ..color = (isNearHorizon ? _dawnOrange : _dayGold).withValues(alpha: 0.8); for (var i = 0; i < 8; i++) { final rayAngle = (i * math.pi / 4); final innerRadius = 14.0; final outerRadius = 18.0; canvas.drawLine( Offset( sunX + innerRadius * math.cos(rayAngle), sunY + innerRadius * math.sin(rayAngle), ), Offset( sunX + outerRadius * math.cos(rayAngle), sunY + outerRadius * math.sin(rayAngle), ), rayPaint, ); } } else { // Moon highlight final moonHighlight = Paint() ..style = PaintingStyle.fill ..color = Colors.white24; canvas.drawCircle(Offset(sunX - 3, sunY - 3), 4, moonHighlight); } // Draw time markers on arc _drawTimeMarkers(canvas, center, radius); } double _calculateSunPosition() { if (sunTimes?.sunrise == null || sunTimes?.sunset == null) { // Default to noon position return 0.5; } final now = DateTime.now(); final sunrise = sunTimes!.sunrise!; final sunset = sunTimes!.sunset!; // Before sunrise if (now.isBefore(sunrise)) { // Calculate position in pre-dawn (negative values = below horizon) final midnight = DateTime(now.year, now.month, now.day); final minutesSinceMidnight = now.difference(midnight).inMinutes; final minutesToSunrise = sunrise.difference(midnight).inMinutes; // Map to 0 at sunrise, negative before return (minutesSinceMidnight / minutesToSunrise) * 0.15 - 0.1; } // After sunset if (now.isAfter(sunset)) { // Calculate position in post-dusk final minutesSinceSunset = now.difference(sunset).inMinutes; final nextMidnight = DateTime(now.year, now.month, now.day + 1); final minutesToMidnight = nextMidnight.difference(sunset).inMinutes; // Map to 1 at sunset, going towards negative return 1.0 + (minutesSinceSunset / minutesToMidnight) * 0.1; } // During daylight hours final totalDaylight = sunset.difference(sunrise).inMinutes; final minutesSinceSunrise = now.difference(sunrise).inMinutes; return minutesSinceSunrise / totalDaylight; } void _drawTimeMarkers(Canvas canvas, Offset center, double radius) { final textPainter = TextPainter( textDirection: TextDirection.ltr, textAlign: TextAlign.center, ); final markerStyle = TextStyle( fontSize: 10, color: isDark ? Colors.white38 : Colors.black38, ); // Draw quarter markers (6am, 12pm, 6pm positions conceptually) final markers = ['6:00', '12:00', '18:00']; final positions = [0.25, 0.5, 0.75]; for (var i = 0; i < markers.length; i++) { final angle = math.pi + (positions[i] * math.pi); final markerRadius = radius + 15; final x = center.dx + markerRadius * math.cos(angle); final y = center.dy + markerRadius * math.sin(angle); textPainter.text = TextSpan(text: markers[i], style: markerStyle); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, y - textPainter.height / 2), ); } } @override bool shouldRepaint(covariant _SunArcPainter oldDelegate) { return oldDelegate.sunTimes != sunTimes || oldDelegate.isDark != isDark; } }