Files
habo/lib/habits/habits_screen.dart
dazhuang aa69f2a91e feat: initial commit - Habo habit tracking app
- Complete MVP with Repository Pattern, SQLite storage
- Provider + ChangeNotifier state management
- Navigation 2.0 with deep link support
- Habit CRUD with twoDayRule, notifications, categories
- Backup/Restore via JSON
- Statistics with streak tracking
- Material You theme support
- Biometric lock support
- Desktop widget support
- 27 languages i18n structure
- Comprehensive test suite (87/89 passing)
2026-04-13 15:02:30 +00:00

336 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:habo/habits/habits_manager.dart';
import 'package:habo/habits/habit.dart';
import 'package:habo/model/habit_data.dart';
import 'package:habo/habits/calendar_column.dart';
import 'package:habo/settings/settings_manager.dart';
import 'package:habo/navigation/app_state_manager.dart';
import 'package:habo/constants.dart';
import 'package:reorderables/reorderables.dart';
class HabitsScreen extends StatefulWidget {
const HabitsScreen({super.key});
@override
State<HabitsScreen> createState() => _HabitsScreenState();
}
class _HabitsScreenState extends State<HabitsScreen> {
@override
Widget build(BuildContext context) {
final habitsManager = context.watch<HabitsManager>();
final settings = context.watch<SettingsManager>();
final appState = context.read<AppStateManager>();
final activeHabits = habitsManager.activeHabits;
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Habo',
style: TextStyle(
color: HaboColors.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Icon(Icons.check_circle, color: HaboColors.primary, size: 20),
],
),
actions: [
IconButton(
icon: const Icon(Icons.bar_chart),
tooltip: 'Statistics',
onPressed: () => appState.goStatistics(true),
),
IconButton(
icon: const Icon(Icons.settings),
tooltip: 'Settings',
onPressed: () => appState.goSettings(true),
),
],
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
),
body: activeHabits.isEmpty
? _buildEmptyState(context)
: _buildHabitList(context, activeHabits),
floatingActionButton: FloatingActionButton(
backgroundColor: HaboColors.primary,
child: const Icon(Icons.add, color: Colors.white),
onPressed: () => appState.goCreateHabit(true),
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/emptyList.svg',
width: 200,
height: 200,
errorBuilder: (_, __, ___) => Icon(
Icons.track_changes,
size: 120,
color: Colors.grey.shade300,
),
),
const SizedBox(height: 24),
Text(
'Empty list',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Create your first habit.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.grey.shade500,
),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () => context.read<AppStateManager>().goCreateHabit(true),
icon: const Icon(Icons.add),
label: const Text('Create your first habit'),
style: ElevatedButton.styleFrom(
backgroundColor: HaboColors.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
],
),
);
}
Widget _buildHabitList(BuildContext context, List<Habit> habits) {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Row(
children: [
Text(
'Habits:',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
'${habits.length}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: HaboColors.primary,
fontWeight: FontWeight.bold,
),
),
],
),
),
Expanded(
child: ReorderableColumn(
crossAxisAlignment: CrossAxisAlignment.start,
children: habits
.map((habit) => Padding(
key: ValueKey(habit.habitData.id),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: HabitCard(habit: habit),
))
.toList(),
onReorder: (oldIndex, newIndex) {
context.read<HabitsManager>().reorderList(oldIndex, newIndex);
},
),
),
],
);
}
}
class HabitCard extends StatelessWidget {
final Habit habit;
const HabitCard({super.key, required this.habit});
@override
Widget build(BuildContext context) {
final data = habit.habitData;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title row
Row(
children: [
Icon(
data.archived ? Icons.archive : Icons.check_circle,
color: data.archived ? Colors.grey : HaboColors.primary,
size: 22,
),
const SizedBox(width: 8),
Expanded(
child: Text(
data.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
// Streak badge
if (data.streakVisible)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: HaboColors.primary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'🔥 ${data.streak} days',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
if (data.orangeStreak)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: HaboColors.orange,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'⚠️ ${data.streak} days',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 8),
// Mini calendar - last 14 days
_buildMiniCalendar(context),
const SizedBox(height: 8),
// Info row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
data.twoDayRule ? '📏 Two Day Rule ON' : '',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade500,
),
),
if (data.habitType == HabitType.numeric)
Text(
'📊 ${data.targetValue} ${data.unit}',
style: TextStyle(
fontSize: 11,
color: Colors.blue.shade600,
),
),
],
),
],
),
),
);
}
Widget _buildMiniCalendar(BuildContext context) {
final today = DateTime.now();
final dots = <Widget>[];
for (int i = 13; i >= 0; i--) {
final date = DateTime(today.year, today.month, today.day - i);
final event = data.events[date];
Color dotColor = Colors.grey.shade300;
String label = _shortDay(date);
if (event != null) {
final dayType = event[0] is DayType ? event[0] as DayType : DayType.values[event[0] as int];
switch (dayType) {
case DayType.check:
dotColor = HaboColors.primary;
break;
case DayType.fail:
dotColor = HaboColors.red;
break;
case DayType.skip:
dotColor = HaboColors.skip;
break;
case DayType.progress:
dotColor = HaboColors.progress;
break;
case DayType.clear:
break;
}
}
// Check if today
final isToday = i == 0;
dots.add(
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: TextStyle(
fontSize: 9,
color: isToday ? HaboColors.primary : Colors.grey.shade500,
fontWeight: isToday ? FontWeight.bold : FontWeight.normal,
),
),
const SizedBox(height: 2),
Container(
width: 18,
height: 18,
decoration: BoxDecoration(
color: dotColor,
shape: BoxShape.circle,
border: isToday
? Border.all(color: HaboColors.primary, width: 2)
: null,
),
),
],
),
);
}
return Wrap(
spacing: 6,
runSpacing: 2,
children: dots,
);
}
String _shortDay(DateTime date) {
const days = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
return days[date.weekday % 7];
}
// Need access to habit data
HabitData get data => habit.habitData;
}