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:
@@ -0,0 +1,229 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/widgets/time_visualizer.dart';
|
||||
|
||||
/// Time perception tools dashboard.
|
||||
///
|
||||
/// - Accuracy trend chart (fl_chart) showing estimate vs actual over time.
|
||||
/// - "You tend to underestimate by X%" insight.
|
||||
/// - Recent time entries list.
|
||||
/// - Tips for improving time awareness.
|
||||
class TimeDashboardScreen extends StatelessWidget {
|
||||
const TimeDashboardScreen({super.key});
|
||||
|
||||
// ── Dummy data for the chart ─────────────────────────────────────
|
||||
static const _estimatedData = [15.0, 20.0, 10.0, 30.0, 25.0, 15.0, 20.0];
|
||||
static const _actualData = [22.0, 18.0, 14.0, 40.0, 28.0, 20.0, 19.0];
|
||||
static const _labels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
|
||||
double get _averageBias {
|
||||
double total = 0;
|
||||
for (int i = 0; i < _estimatedData.length; i++) {
|
||||
total += (_actualData[i] - _estimatedData[i]) / _estimatedData[i];
|
||||
}
|
||||
return (total / _estimatedData.length) * 100;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final bias = _averageBias;
|
||||
final biasText = bias > 0
|
||||
? 'You tend to underestimate by ${bias.abs().toStringAsFixed(0)}%'
|
||||
: 'You tend to overestimate by ${bias.abs().toStringAsFixed(0)}%';
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
title: const Text('Time Perception'),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// ── Insight card ─────────────────────────────────────────
|
||||
Card(
|
||||
color: AppColors.primaryLight.withAlpha(30),
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.insights_rounded, size: 32, color: AppColors.primary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(biasText, style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Trend chart ──────────────────────────────────────────
|
||||
Text('Estimated vs Actual (this week)', style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: const FlGridData(show: false),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 32,
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
'${value.toInt()}m',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final idx = value.toInt();
|
||||
if (idx < 0 || idx >= _labels.length) return const SizedBox.shrink();
|
||||
return Text(_labels[idx], style: theme.textTheme.bodySmall);
|
||||
},
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
// Estimated
|
||||
LineChartBarData(
|
||||
spots: List.generate(
|
||||
_estimatedData.length,
|
||||
(i) => FlSpot(i.toDouble(), _estimatedData[i]),
|
||||
),
|
||||
isCurved: true,
|
||||
color: AppColors.primary,
|
||||
barWidth: 3,
|
||||
dotData: const FlDotData(show: true),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: AppColors.primary.withAlpha(20),
|
||||
),
|
||||
),
|
||||
// Actual
|
||||
LineChartBarData(
|
||||
spots: List.generate(
|
||||
_actualData.length,
|
||||
(i) => FlSpot(i.toDouble(), _actualData[i]),
|
||||
),
|
||||
isCurved: true,
|
||||
color: AppColors.secondary,
|
||||
barWidth: 3,
|
||||
dotData: const FlDotData(show: true),
|
||||
dashArray: [6, 4],
|
||||
),
|
||||
],
|
||||
minY: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// ── Legend ────────────────────────────────────────────────
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_LegendDot(color: AppColors.primary, label: 'Estimated'),
|
||||
const SizedBox(width: 20),
|
||||
_LegendDot(color: AppColors.secondary, label: 'Actual'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Recent entries ───────────────────────────────────────
|
||||
Text('Recent', style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
...List.generate(
|
||||
_estimatedData.length,
|
||||
(i) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: TimeVisualizer(
|
||||
estimatedMinutes: _estimatedData[i].toInt(),
|
||||
actualMinutes: _actualData[i].toInt(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Tips ─────────────────────────────────────────────────
|
||||
Text('Tips for improving time awareness', style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
const _TipCard(
|
||||
icon: Icons.timer_outlined,
|
||||
text: 'Before starting, say your estimate out loud — it makes you more committed.',
|
||||
),
|
||||
const _TipCard(
|
||||
icon: Icons.visibility_rounded,
|
||||
text: 'Use a visible clock or timer while working to build a sense of passing time.',
|
||||
),
|
||||
const _TipCard(
|
||||
icon: Icons.edit_note_rounded,
|
||||
text: 'After each task, note how long it really took. Patterns will emerge!',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LegendDot extends StatelessWidget {
|
||||
final Color color;
|
||||
final String label;
|
||||
const _LegendDot({required this.color, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TipCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
const _TipCard({required this.icon, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: AppColors.surfaceVariantLight,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 22, color: AppColors.primary),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(text, style: theme.textTheme.bodyMedium)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user