165 lines
4.7 KiB
Dart
165 lines
4.7 KiB
Dart
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<double> 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;
|
|
}
|