Files
habo/lib/habits/create_habit.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

344 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:habo/habits/habits_manager.dart';
import 'package:habo/navigation/app_state_manager.dart';
import 'package:habo/constants.dart';
class CreateHabitScreen extends StatefulWidget {
const CreateHabitScreen({super.key});
@override
State<CreateHabitScreen> createState() => _CreateHabitScreenState();
}
class _CreateHabitScreenState extends State<CreateHabitScreen> {
final _titleController = TextEditingController();
final _cueController = TextEditingController();
final _routineController = TextEditingController();
final _rewardController = TextEditingController();
final _sanctionController = TextEditingController();
final _accountantController = TextEditingController();
final _targetValueController = TextEditingController(text: '100');
final _partialValueController = TextEditingController(text: '10');
final _unitController = TextEditingController();
bool _twoDayRule = false;
bool _notification = false;
bool _advanced = false;
bool _habitContract = false;
bool _showReward = false;
bool _showSanction = false;
HabitType _habitType = HabitType.boolean;
TimeOfDay _notTime = const TimeOfDay(hour: 20, minute: 0);
@override
void dispose() {
_titleController.dispose();
_cueController.dispose();
_routineController.dispose();
_rewardController.dispose();
_sanctionController.dispose();
_accountantController.dispose();
_targetValueController.dispose();
_partialValueController.dispose();
_unitController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Create Habit'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.read<AppStateManager>().goCreateHabit(false),
),
actions: [
IconButton(
icon: const Icon(Icons.check, color: HaboColors.primary),
onPressed: _save,
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Title
TextField(
controller: _titleController,
autofocus: true,
decoration: const InputDecoration(
labelText: 'Title *',
hintText: 'e.g. Exercise, Reading, Meditation',
prefixIcon: Icon(Icons.title),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// Habit Type
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Habit Type',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
RadioListTile<HabitType>(
title: const Text('Checkable (Yes/No)'),
subtitle: const Text('Simple daily check-in'),
secondary: const Icon(Icons.check_box),
value: HabitType.boolean,
groupValue: _habitType,
onChanged: (v) => setState(() => _habitType = v!),
),
RadioListTile<HabitType>(
title: const Text('Progressive (Numeric)'),
subtitle: const Text('Track counts like steps, glasses'),
secondary: const Icon(Icons.track_changes),
value: HabitType.numeric,
groupValue: _habitType,
onChanged: (v) => setState(() => _habitType = v!),
),
],
),
),
),
// Numeric fields
if (_habitType == HabitType.numeric)
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Progress Settings',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
TextField(
controller: _targetValueController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: 'Target Value',
prefixIcon: Icon(Icons.flag),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _partialValueController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: 'Partial Value (increment)',
prefixIcon: Icon(Icons.add),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _unitController,
decoration: const InputDecoration(
labelText: 'Unit',
hintText: 'e.g. steps, km, glasses',
prefixIcon: Icon(Icons.straighten),
border: OutlineInputBorder(),
),
),
],
),
),
),
// Two Day Rule
Card(
child: SwitchListTile(
title: const Text('Two Day Rule'),
subtitle: const Text('Allow one miss without losing streak'),
secondary: const Icon(Icons.rule),
value: _twoDayRule,
onChanged: (v) => setState(() => _twoDayRule = v),
),
),
// Notifications
Card(
child: Column(
children: [
SwitchListTile(
title: const Text('Daily Reminder'),
subtitle: const Text('Get notified to check habits'),
secondary: const Icon(Icons.notifications),
value: _notification,
onChanged: (v) => setState(() => _notification = v),
),
if (_notification)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: OutlinedButton.icon(
onPressed: _pickTime,
icon: const Icon(Icons.access_time),
label: Text('${_notTime.format(context)}'),
),
),
],
),
),
// Advanced Options
ExpansionTile(
leading: const Icon(Icons.tune),
title: const Text('Advanced Options'),
subtitle: const Text('Cue, Routine, Reward, Habit Contract'),
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _cueController,
decoration: const InputDecoration(
labelText: 'Cue (Trigger)',
hintText: 'e.g. At 7:00 AM',
prefixIcon: Icon(Icons.flash_on),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _routineController,
decoration: const InputDecoration(
labelText: 'Routine (Action)',
hintText: 'e.g. Do 50 push ups',
prefixIcon: Icon(Icons.fitness_center),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _rewardController,
decoration: const InputDecoration(
labelText: 'Reward',
hintText: 'e.g. 15 min games',
prefixIcon: Icon(Icons.emoji_events),
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 12),
Switch(value: _showReward, onChanged: (v) => setState(() => _showReward = v)),
],
),
],
),
),
const Divider(),
SwitchListTile(
title: const Text('Habit Contract'),
subtitle: const Text('Set a sanction for missing'),
secondary: const Icon(Icons.gavel),
value: _habitContract,
onChanged: (v) => setState(() => _habitContract = v),
),
if (_habitContract)
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _sanctionController,
decoration: const InputDecoration(
labelText: 'Sanction',
hintText: 'e.g. Donate \$10 to charity',
prefixIcon: Icon(Icons.warning),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _accountantController,
decoration: const InputDecoration(
labelText: 'Accountability Partner',
hintText: 'e.g. Dan',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
),
],
),
),
],
),
const SizedBox(height: 24),
// Save button
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
onPressed: _save,
icon: const Icon(Icons.check),
label: const Text('Save Habit', style: TextStyle(fontSize: 16)),
style: ElevatedButton.styleFrom(
backgroundColor: HaboColors.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
const SizedBox(height: 24),
],
),
);
}
Future<void> _pickTime() async {
final picked = await showTimePicker(context: context, initialTime: _notTime);
if (picked != null) setState(() => _notTime = picked);
}
void _save() {
if (_titleController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Title cannot be empty!'), backgroundColor: HaboColors.red),
);
return;
}
context.read<HabitsManager>().addHabit(
_titleController.text.trim(),
_twoDayRule,
_cueController.text,
_routineController.text,
_rewardController.text,
_showReward,
_advanced || _habitContract,
_notification,
_notTime,
_sanctionController.text,
_showSanction,
_accountantController.text,
habitType: _habitType,
targetValue: double.tryParse(_targetValueController.text) ?? 100.0,
partialValue: double.tryParse(_partialValueController.text) ?? 10.0,
unit: _unitController.text,
);
context.read<AppStateManager>().goCreateHabit(false);
}
}