This commit is contained in:
2026-02-26 20:49:21 -05:00
parent bebcf48a32
commit 4d9e715361
243 changed files with 25648 additions and 14573 deletions
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class EmptyState extends StatelessWidget {
const EmptyState({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.checklist, size: 80, color: Colors.grey.shade400),
const SizedBox(height: 16),
Text(
'No todos yet!\nTap + to add one',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.grey.shade600),
),
],
),
);
}
}
+36
View File
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../state/todo_state.dart';
class FilterBar extends StatelessWidget {
const FilterBar({super.key});
@override
Widget build(BuildContext context) {
final state = context.watch<TodoState>();
final counts = state.counts;
final filters = [
(TodoFilter.all, 'All', 'all'),
(TodoFilter.active, 'Active', 'active'),
(TodoFilter.completed, 'Done', 'completed'),
];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: filters.map((entry) {
final (filterVal, label, countKey) = entry;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
selected: state.filter == filterVal,
label: Text('$label (${counts[countKey] ?? 0})'),
onSelected: (_) => state.setFilter(filterVal),
),
);
}).toList(),
),
);
}
}
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import '../state/todo_state.dart';
class PriorityBadge extends StatelessWidget {
final String priority;
const PriorityBadge({super.key, required this.priority});
@override
Widget build(BuildContext context) {
final colorValue = priorityColors[priority] ?? 0xFF999999;
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: Color(colorValue),
shape: BoxShape.circle,
),
);
}
}
+90
View File
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/todo.dart';
import '../state/todo_state.dart';
Future<void> showTodoDialog(BuildContext context, Todo? todo) async {
final editing = todo != null;
final titleCtl = TextEditingController(text: todo?.title ?? '');
String selectedCategory = todo?.category ?? 'Personal';
String selectedPriority = todo?.priority ?? 'medium';
await showDialog(
context: context,
builder: (dialogCtx) {
return StatefulBuilder(
builder: (ctx, setState) {
return AlertDialog(
title: Text(editing ? 'Edit Todo' : 'Add New Todo'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: titleCtl,
decoration: const InputDecoration(
labelText: 'What needs to be done?',
border: OutlineInputBorder(),
),
autofocus: true,
),
const SizedBox(height: 16),
const Text('Category',
style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: categories.map((c) {
return ChoiceChip(
label: Text(c),
selected: c == selectedCategory,
onSelected: (_) {
setState(() => selectedCategory = c);
},
);
}).toList(),
),
const SizedBox(height: 16),
const Text('Priority',
style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
SegmentedButton<String>(
segments: priorityKeys.map((p) {
return ButtonSegment(value: p, label: Text(p));
}).toList(),
selected: {selectedPriority},
onSelectionChanged: (selection) {
setState(() => selectedPriority = selection.first);
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogCtx).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
final title = titleCtl.text.trim();
if (title.isEmpty) return;
final state = context.read<TodoState>();
if (editing) {
state.updateTodo(
todo!.id!, title, selectedCategory, selectedPriority);
} else {
state.addTodo(title, selectedCategory, selectedPriority);
}
Navigator.of(dialogCtx).pop();
},
child: Text(editing ? 'Save' : 'Add'),
),
],
);
},
);
},
);
}
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/todo.dart';
import '../state/todo_state.dart';
import 'priority_badge.dart';
import 'todo_form.dart';
class TodoItem extends StatelessWidget {
final Todo todo;
const TodoItem({super.key, required this.todo});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: Checkbox(
value: todo.completed,
onChanged: (_) {
context.read<TodoState>().toggleTodo(todo.id!);
},
),
title: Text(
todo.title,
style: TextStyle(
decoration:
todo.completed ? TextDecoration.lineThrough : TextDecoration.none,
color: todo.completed ? colorScheme.onSurfaceVariant : null,
),
),
subtitle: Row(
children: [
Chip(
label: Text(todo.category, style: const TextStyle(fontSize: 12)),
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
),
const SizedBox(width: 8),
PriorityBadge(priority: todo.priority),
],
),
onTap: () => showTodoDialog(context, todo),
),
);
}
}
+41
View File
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../state/todo_state.dart';
import 'todo_item.dart';
import 'empty_state.dart';
class TodoList extends StatelessWidget {
const TodoList({super.key});
@override
Widget build(BuildContext context) {
final state = context.watch<TodoState>();
final todos = state.filteredTodos;
if (todos.isEmpty) {
return const EmptyState();
}
return ListView.builder(
padding: const EdgeInsets.only(bottom: 80),
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return Dismissible(
key: ValueKey(todo.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
color: Colors.red,
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) {
state.deleteTodo(todo.id!);
},
child: TodoItem(todo: todo),
);
},
);
}
}