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>
This commit is contained in:
125
lib/core/widgets/time_visualizer.dart
Normal file
125
lib/core/widgets/time_visualizer.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
/// Visual time block widget showing estimated vs actual duration.
|
||||
///
|
||||
/// - Horizontal bar: green (under estimate) -> yellow (near) -> soft orange (over).
|
||||
/// - NO red — going over is shown gently, not punitively.
|
||||
class TimeVisualizer extends StatelessWidget {
|
||||
/// Estimated duration in minutes.
|
||||
final int estimatedMinutes;
|
||||
|
||||
/// Actual duration in minutes (null if not yet completed).
|
||||
final int? actualMinutes;
|
||||
|
||||
/// Optional height for the bars.
|
||||
final double barHeight;
|
||||
|
||||
const TimeVisualizer({
|
||||
super.key,
|
||||
required this.estimatedMinutes,
|
||||
this.actualMinutes,
|
||||
this.barHeight = 12,
|
||||
});
|
||||
|
||||
/// Returns a color based on actual / estimated ratio.
|
||||
/// <= 0.8 -> green (under)
|
||||
/// 0.8-1.1 -> teal (on time)
|
||||
/// 1.1-1.5 -> yellow (slightly over)
|
||||
/// > 1.5 -> soft orange (over, but gentle)
|
||||
Color _barColor(double ratio) {
|
||||
if (ratio <= 0.8) return AppColors.completed;
|
||||
if (ratio <= 1.1) return AppColors.primary;
|
||||
if (ratio <= 1.5) return AppColors.energyMedium;
|
||||
return AppColors.energyHigh; // soft orange, NOT red
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final actual = actualMinutes;
|
||||
final hasActual = actual != null && actual > 0;
|
||||
|
||||
// If no actual yet, show just the estimate bar.
|
||||
final ratio = hasActual ? actual / estimatedMinutes : 0.0;
|
||||
final estimateWidth = 1.0; // always full width = estimate
|
||||
final actualWidth = hasActual ? ratio.clamp(0.0, 2.0) / 2.0 : 0.0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Labels
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Estimated: $estimatedMinutes min',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
if (hasActual)
|
||||
Text(
|
||||
'Actual: $actual min',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _barColor(ratio),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// ── Estimate bar (background / reference) ──────────────────
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(barHeight / 2),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Full estimate background
|
||||
Container(
|
||||
height: barHeight,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(barHeight / 2),
|
||||
),
|
||||
),
|
||||
|
||||
// Actual fill
|
||||
if (hasActual)
|
||||
FractionallySizedBox(
|
||||
widthFactor: actualWidth.clamp(0.0, estimateWidth),
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: _barColor(ratio),
|
||||
borderRadius: BorderRadius.circular(barHeight / 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── Gentle insight ─────────────────────────────────────────
|
||||
if (hasActual) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_insightText(ratio),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: _barColor(ratio),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _insightText(double ratio) {
|
||||
if (ratio <= 0.8) return 'Finished early — nice!';
|
||||
if (ratio <= 1.1) return 'Right on target.';
|
||||
if (ratio <= 1.5) return 'A little over — totally fine.';
|
||||
return 'Took longer than expected. That happens!';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user