import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../theme/app_theme.dart'; import '../models/app_settings.dart'; class RotaryKnob extends StatelessWidget { final double size; final double value; final double min; final double max; final bool isTiming; final double currentSeconds; final ValueChanged onChanged; const RotaryKnob({ super.key, required this.size, required this.value, required this.min, required this.max, required this.isTiming, required this.currentSeconds, required this.onChanged, }); @override Widget build(BuildContext context) { final displaySeconds = isTiming ? currentSeconds.toInt() : (value * 60).toInt(); final minutesStr = (displaySeconds / 60).floor().toString().padLeft(2, '0'); final secondsStr = (displaySeconds % 60).toString().padLeft(2, '0'); final double progress = isTiming ? currentSeconds / (value * 60) : (value - min) / (max - min); return GestureDetector( onPanUpdate: (details) { if (isTiming) return; double sensitivity = 0.5; double newValue = value - (details.delta.dy * sensitivity); newValue = newValue.clamp(min, max); if (newValue.round() != value.round() && AppSettings.enableHaptics) { HapticFeedback.selectionClick(); } onChanged(newValue); }, child: Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, color: AppTheme.bgLight, boxShadow: isTiming ? [] : AppTheme.softShadow, ), child: CustomPaint( painter: KnobProgressPainter( progress: progress, color: isTiming ? AppTheme.flowStart : AppTheme.primary, isTiming: isTiming, ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( minutesStr, style: TextStyle( fontSize: size * 0.32, fontWeight: FontWeight.w700, color: isTiming ? Colors.white : AppTheme.primary, height: 1.0, letterSpacing: -2.0, shadows: isTiming ? [const Shadow(blurRadius: 8, color: Colors.black54)] : null, ), ), Text( secondsStr, style: TextStyle( fontSize: size * 0.12, fontWeight: FontWeight.w600, color: isTiming ? Colors.white70 : AppTheme.textSub, letterSpacing: 1.0, ), ), if (!isTiming) const Padding( padding: EdgeInsets.only(top: 8.0), child: Text( "DRAG TO SET", style: TextStyle( fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 2, color: Colors.grey, ), ), ) ], ), ), ), ), ); } } class KnobProgressPainter extends CustomPainter { final double progress; final Color color; final bool isTiming; KnobProgressPainter({ required this.progress, required this.color, required this.isTiming, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = size.width / 2 - 10; final trackPaint = Paint() ..color = Colors.grey.withOpacity(0.2) ..style = PaintingStyle.stroke ..strokeWidth = 10; canvas.drawCircle(center, radius, trackPaint); final progressPaint = Paint() ..color = color ..style = PaintingStyle.stroke ..strokeWidth = 10 ..strokeCap = StrokeCap.round; final sweepAngle = 2 * math.pi * (isTiming ? progress : (progress * 0.75)); canvas.drawArc( Rect.fromCircle(center: center, radius: radius), -math.pi / 2, sweepAngle, false, progressPaint, ); if (!isTiming) { final knobAngle = -math.pi / 2 + sweepAngle; final knobCenter = Offset( center.dx + radius * math.cos(knobAngle), center.dy + radius * math.sin(knobAngle), ); canvas.drawCircle(knobCenter, 14, Paint()..color = Colors.white); canvas.drawCircle(knobCenter, 5, Paint()..color = color); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }