Files
focusflow/lib/features/time_perception/presentation/widgets/gentle_timer.dart
Oracle Public Cloud User 50931d839d Initial scaffold: FocusFlow ADHD Task Manager Flutter app
BLoC/Cubit state management, ADHD-friendly theme (calming teal, no red),
GetIt DI, GoRouter navigation. Screens: task dashboard, focus mode,
task create/detail, streaks, time perception, settings, onboarding, auth.
Custom widgets: TaskCard, RewardPopup, StreakRing, GentleNudgeCard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:53:58 +00:00

160 lines
4.1 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../../../../core/theme/app_colors.dart';
/// Non-anxiety-inducing timer widget.
///
/// Design rules:
/// - Circular progress (NOT countdown — progress forward).
/// - Soft pulsing animation.
/// - Color stays calm (teal/blue).
/// - No alarming sounds or red colors.
/// - Shows "X min so far" not "X min remaining."
class GentleTimer extends StatefulWidget {
/// Total elapsed seconds.
final int elapsedSeconds;
/// Optional estimated total in seconds (used only for the progress ring).
final int? estimatedTotalSeconds;
/// Widget size (width & height).
final double size;
const GentleTimer({
super.key,
required this.elapsedSeconds,
this.estimatedTotalSeconds,
this.size = 180,
});
@override
State<GentleTimer> createState() => _GentleTimerState();
}
class _GentleTimerState extends State<GentleTimer>
with SingleTickerProviderStateMixin {
late final AnimationController _pulseController;
late final Animation<double> _pulse;
@override
void initState() {
super.initState();
_pulseController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
_pulse = Tween<double>(begin: 0.96, end: 1.0).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_pulseController.dispose();
super.dispose();
}
String _formatElapsed() {
final m = widget.elapsedSeconds ~/ 60;
final s = widget.elapsedSeconds % 60;
if (m == 0) return '${s}s so far';
return '${m}m ${s.toString().padLeft(2, '0')}s so far';
}
double get _progress {
final total = widget.estimatedTotalSeconds;
if (total == null || total == 0) {
// No estimate — use a slow modular fill so the ring still moves.
return (widget.elapsedSeconds % 300) / 300;
}
return (widget.elapsedSeconds / total).clamp(0.0, 1.0);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AnimatedBuilder(
animation: _pulse,
builder: (context, child) {
return Transform.scale(
scale: _pulse.value,
child: child,
);
},
child: SizedBox(
width: widget.size,
height: widget.size,
child: CustomPaint(
painter: _GentleTimerPainter(
progress: _progress,
color: AppColors.primary,
backgroundColor: AppColors.primary.withAlpha(25),
),
child: Center(
child: Text(
_formatElapsed(),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
textAlign: TextAlign.center,
),
),
),
),
);
}
}
class _GentleTimerPainter extends CustomPainter {
final double progress;
final Color color;
final Color backgroundColor;
_GentleTimerPainter({
required this.progress,
required this.color,
required this.backgroundColor,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.shortestSide / 2) - 10;
const strokeWidth = 10.0;
const startAngle = -math.pi / 2;
// Background circle
canvas.drawCircle(
center,
radius,
Paint()
..color = backgroundColor
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round,
);
// Progress arc
if (progress > 0) {
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
2 * math.pi * progress,
false,
Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round,
);
}
}
@override
bool shouldRepaint(covariant _GentleTimerPainter oldDelegate) =>
progress != oldDelegate.progress;
}