Files
focusflow/lib/features/streaks/presentation/screens/streaks_screen.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

230 lines
7.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:focusflow_shared/focusflow_shared.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/streak_ring.dart';
import '../bloc/streak_bloc.dart';
/// Streak overview screen.
///
/// - Active streaks as cards with [StreakRing] widget.
/// - "Frozen" indicator for paused streaks.
/// - Tap to see history.
/// - "New Streak" FAB.
/// - Grace day info shown positively.
class StreaksScreen extends StatefulWidget {
const StreaksScreen({super.key});
@override
State<StreaksScreen> createState() => _StreaksScreenState();
}
class _StreaksScreenState extends State<StreaksScreen> {
late final StreakBloc _bloc;
@override
void initState() {
super.initState();
_bloc = StreakBloc()..add(const StreaksLoaded());
}
@override
void dispose() {
_bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocProvider.value(
value: _bloc,
child: Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_rounded),
onPressed: () => context.go('/'),
),
title: const Text('Your Streaks'),
),
body: BlocBuilder<StreakBloc, StreakState>(
builder: (context, state) {
if (state is StreakLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is StreakError) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off_rounded, size: 48, color: AppColors.skipped),
const SizedBox(height: 12),
Text(state.message, style: theme.textTheme.bodyLarge, textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton(
onPressed: () => _bloc.add(const StreaksLoaded()),
child: const Text('Retry'),
),
],
),
),
);
}
if (state is StreakLoaded) {
if (state.streaks.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.local_fire_department_rounded,
size: 56, color: AppColors.tertiary),
const SizedBox(height: 12),
Text('No streaks yet',
style: theme.textTheme.titleMedium),
const SizedBox(height: 4),
Text(
'Start a streak to build consistency — with grace days built in.',
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
],
),
),
);
}
return ListView.builder(
padding: const EdgeInsets.only(top: 8, bottom: 100),
itemCount: state.streaks.length,
itemBuilder: (context, index) {
final streak = state.streaks[index];
return _StreakCard(
streak: streak,
onComplete: () => _bloc.add(StreakCompleted(streak.id)),
);
},
);
}
return const SizedBox.shrink();
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
// TODO: navigate to create streak screen.
},
icon: const Icon(Icons.add_rounded),
label: const Text('New Streak'),
),
),
);
}
}
class _StreakCard extends StatelessWidget {
final Streak streak;
final VoidCallback onComplete;
const _StreakCard({required this.streak, required this.onComplete});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isFrozen = streak.frozenUntil != null &&
streak.frozenUntil!.isAfter(DateTime.now());
final graceDaysRemaining = streak.graceDays - streak.graceUsed;
return Card(
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {
// TODO: navigate to streak history.
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
StreakRing(
currentCount: streak.currentCount,
graceDaysRemaining: graceDaysRemaining,
totalGraceDays: streak.graceDays,
isFrozen: isFrozen,
size: 72,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
streak.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
if (isFrozen)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primaryLight.withAlpha(40),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.ac_unit_rounded, size: 14, color: AppColors.primaryLight),
SizedBox(width: 4),
Text('Frozen',
style: TextStyle(fontSize: 12, color: AppColors.primaryLight)),
],
),
),
],
),
const SizedBox(height: 4),
Text(
'Current: ${streak.currentCount} days | Best: ${streak.longestCount}',
style: theme.textTheme.bodySmall,
),
if (graceDaysRemaining > 0 && !isFrozen)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'$graceDaysRemaining grace days remaining',
style: theme.textTheme.bodySmall?.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
if (!isFrozen)
IconButton(
icon: const Icon(Icons.check_circle_outline_rounded,
color: AppColors.completed),
onPressed: onComplete,
tooltip: 'Complete today',
),
],
),
),
),
);
}
}