335 lines
10 KiB
Dart
335 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'dart:math' as math;
|
|
import '../theme/app_theme.dart';
|
|
import '../models/cycle_entry.dart';
|
|
|
|
<<<<<<< HEAD
|
|
class CycleRing extends StatelessWidget {
|
|
=======
|
|
class CycleRing extends StatefulWidget {
|
|
>>>>>>> 6742220 (Your commit message here)
|
|
final int dayOfCycle;
|
|
final int totalDays;
|
|
final CyclePhase phase;
|
|
|
|
const CycleRing({
|
|
super.key,
|
|
required this.dayOfCycle,
|
|
required this.totalDays,
|
|
required this.phase,
|
|
});
|
|
|
|
@override
|
|
<<<<<<< HEAD
|
|
Widget build(BuildContext context) {
|
|
final progress = dayOfCycle / totalDays;
|
|
final daysUntilNextPeriod = totalDays - dayOfCycle;
|
|
|
|
return Container(
|
|
width: 220,
|
|
height: 220,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
// Background ring
|
|
CustomPaint(
|
|
size: const Size(220, 220),
|
|
painter: _CycleRingPainter(
|
|
progress: progress,
|
|
phase: phase,
|
|
),
|
|
),
|
|
|
|
// Center content
|
|
Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'Day $dayOfCycle',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 32,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.charcoal,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: _getPhaseColor(phase).withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
phase.emoji,
|
|
style: const TextStyle(fontSize: 14),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
phase.label,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: _getPhaseColor(phase),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
daysUntilNextPeriod > 0
|
|
? '$daysUntilNextPeriod days until period'
|
|
: 'Period expected',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 12,
|
|
color: AppColors.warmGray,
|
|
=======
|
|
State<CycleRing> createState() => _CycleRingState();
|
|
}
|
|
|
|
class _CycleRingState extends State<CycleRing> with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1500),
|
|
);
|
|
_animation = CurvedAnimation(
|
|
parent: _controller,
|
|
curve: Curves.easeOutCubic,
|
|
);
|
|
_controller.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final targetProgress = widget.dayOfCycle / widget.totalDays;
|
|
final daysUntilNextPeriod = widget.totalDays - widget.dayOfCycle;
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
final currentProgress = targetProgress * _animation.value;
|
|
|
|
return SizedBox(
|
|
width: 220,
|
|
height: 220,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
// Background ring
|
|
CustomPaint(
|
|
size: const Size(220, 220),
|
|
painter: _CycleRingPainter(
|
|
progress: currentProgress,
|
|
phase: widget.phase,
|
|
isDark: isDark,
|
|
),
|
|
),
|
|
|
|
// Center content with scale and fade animation
|
|
Transform.scale(
|
|
scale: 0.8 + (0.2 * _animation.value),
|
|
child: Opacity(
|
|
opacity: _animation.value,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'Day ${widget.dayOfCycle}',
|
|
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
|
fontSize: 32,
|
|
fontWeight: FontWeight.w600,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: _getPhaseColor(widget.phase).withOpacity(isDark ? 0.3 : 0.2),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: isDark ? Border.all(color: _getPhaseColor(widget.phase).withOpacity(0.5)) : null,
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
widget.phase.emoji,
|
|
style: const TextStyle(fontSize: 14),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
widget.phase.label,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: isDark ? Colors.white : _getPhaseColor(widget.phase),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
daysUntilNextPeriod > 0
|
|
? '$daysUntilNextPeriod days until period'
|
|
: 'Period expected',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
fontSize: 12,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
>>>>>>> 6742220 (Your commit message here)
|
|
),
|
|
),
|
|
],
|
|
),
|
|
<<<<<<< HEAD
|
|
],
|
|
),
|
|
=======
|
|
);
|
|
},
|
|
>>>>>>> 6742220 (Your commit message here)
|
|
);
|
|
}
|
|
|
|
Color _getPhaseColor(CyclePhase phase) {
|
|
switch (phase) {
|
|
case CyclePhase.menstrual:
|
|
return AppColors.menstrualPhase;
|
|
case CyclePhase.follicular:
|
|
return AppColors.follicularPhase;
|
|
case CyclePhase.ovulation:
|
|
return AppColors.ovulationPhase;
|
|
case CyclePhase.luteal:
|
|
return AppColors.lutealPhase;
|
|
}
|
|
}
|
|
}
|
|
|
|
class _CycleRingPainter extends CustomPainter {
|
|
final double progress;
|
|
final CyclePhase phase;
|
|
<<<<<<< HEAD
|
|
|
|
_CycleRingPainter({required this.progress, required this.phase});
|
|
=======
|
|
final bool isDark;
|
|
|
|
_CycleRingPainter({
|
|
required this.progress,
|
|
required this.phase,
|
|
required this.isDark,
|
|
});
|
|
>>>>>>> 6742220 (Your commit message here)
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final center = Offset(size.width / 2, size.height / 2);
|
|
final radius = size.width / 2 - 15;
|
|
const strokeWidth = 12.0;
|
|
|
|
// Background arc
|
|
final bgPaint = Paint()
|
|
<<<<<<< HEAD
|
|
..color = AppColors.lightGray.withOpacity(0.2)
|
|
=======
|
|
..color = (isDark ? Colors.white : AppColors.lightGray).withOpacity(isDark ? 0.05 : 0.1)
|
|
>>>>>>> 6742220 (Your commit message here)
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = strokeWidth
|
|
..strokeCap = StrokeCap.round;
|
|
|
|
canvas.drawCircle(center, radius, bgPaint);
|
|
|
|
// Progress arc
|
|
final progressPaint = Paint()
|
|
..shader = SweepGradient(
|
|
startAngle: -math.pi / 2,
|
|
endAngle: math.pi * 1.5,
|
|
colors: _getGradientColors(phase),
|
|
stops: const [0.0, 0.5, 1.0],
|
|
).createShader(Rect.fromCircle(center: center, radius: radius))
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = strokeWidth
|
|
..strokeCap = StrokeCap.round;
|
|
|
|
canvas.drawArc(
|
|
Rect.fromCircle(center: center, radius: radius),
|
|
-math.pi / 2,
|
|
2 * math.pi * progress,
|
|
false,
|
|
progressPaint,
|
|
);
|
|
|
|
// Dot at current position
|
|
final dotAngle = -math.pi / 2 + 2 * math.pi * progress;
|
|
final dotX = center.dx + radius * math.cos(dotAngle);
|
|
final dotY = center.dy + radius * math.sin(dotAngle);
|
|
|
|
final dotPaint = Paint()
|
|
<<<<<<< HEAD
|
|
..color = Colors.white
|
|
=======
|
|
..color = isDark ? const Color(0xFF1E1E1E) : Colors.white
|
|
>>>>>>> 6742220 (Your commit message here)
|
|
..style = PaintingStyle.fill;
|
|
|
|
final dotBorderPaint = Paint()
|
|
..color = _getPhaseColor(phase)
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 3;
|
|
|
|
canvas.drawCircle(Offset(dotX, dotY), 8, dotPaint);
|
|
canvas.drawCircle(Offset(dotX, dotY), 8, dotBorderPaint);
|
|
}
|
|
|
|
List<Color> _getGradientColors(CyclePhase phase) {
|
|
switch (phase) {
|
|
case CyclePhase.menstrual:
|
|
return [AppColors.rose, AppColors.menstrualPhase, AppColors.blushPink];
|
|
case CyclePhase.follicular:
|
|
return [AppColors.sageGreen, AppColors.follicularPhase, AppColors.sageGreen.withOpacity(0.7)];
|
|
case CyclePhase.ovulation:
|
|
return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose];
|
|
case CyclePhase.luteal:
|
|
return [AppColors.lutealPhase, AppColors.lavender, AppColors.blushPink];
|
|
}
|
|
}
|
|
|
|
Color _getPhaseColor(CyclePhase phase) {
|
|
switch (phase) {
|
|
case CyclePhase.menstrual:
|
|
return AppColors.menstrualPhase;
|
|
case CyclePhase.follicular:
|
|
return AppColors.follicularPhase;
|
|
case CyclePhase.ovulation:
|
|
return AppColors.ovulationPhase;
|
|
case CyclePhase.luteal:
|
|
return AppColors.lutealPhase;
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
|
}
|