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
+70
View File
@@ -0,0 +1,70 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'models/todo.dart';
class TodoDatabase {
static Database? _db;
static Future<void> init() async {
final dir = await getApplicationDocumentsDirectory();
final dbPath = p.join(dir.path, 'todos.db');
_db = await openDatabase(
dbPath,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
category TEXT DEFAULT 'Personal',
priority TEXT DEFAULT 'medium',
created_at INTEGER DEFAULT (strftime('%s','now'))
)
''');
},
);
}
static Future<List<Todo>> loadTodos() async {
if (_db == null) return [];
final rows = await _db!.query('todos', orderBy: 'created_at DESC');
return rows.map((row) => Todo.fromMap(row)).toList();
}
static Future<int> insertTodo(String title, String category, String priority) async {
if (_db == null) return -1;
return await _db!.insert('todos', {
'title': title,
'category': category,
'priority': priority,
'completed': 0,
});
}
static Future<void> updateTodo(int id, String title, String category, String priority) async {
if (_db == null) return;
await _db!.update(
'todos',
{'title': title, 'category': category, 'priority': priority},
where: 'id = ?',
whereArgs: [id],
);
}
static Future<void> toggleTodo(int id, bool completed) async {
if (_db == null) return;
await _db!.update(
'todos',
{'completed': completed ? 1 : 0},
where: 'id = ?',
whereArgs: [id],
);
}
static Future<void> deleteTodo(int id) async {
if (_db == null) return;
await _db!.delete('todos', where: 'id = ?', whereArgs: [id]);
}
}
+30 -1
View File
@@ -1 +1,30 @@
export "cljd-out/todo/main.dart" show main;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'db.dart';
import 'state/todo_state.dart';
import 'screens/home_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await TodoDatabase.init();
final todos = await TodoDatabase.loadTodos();
final todoState = TodoState();
todoState.setTodos(todos);
runApp(
ChangeNotifierProvider.value(
value: todoState,
child: MaterialApp(
title: 'Dart Todo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.deepPurple,
useMaterial3: true,
brightness: Brightness.light,
),
home: const HomeScreen(),
),
),
);
}
+55
View File
@@ -0,0 +1,55 @@
class Todo {
final int? id;
final String title;
final bool completed;
final String category;
final String priority;
final int? createdAt;
Todo({
this.id,
required this.title,
this.completed = false,
this.category = 'Personal',
this.priority = 'medium',
this.createdAt,
});
Todo copyWith({
int? id,
String? title,
bool? completed,
String? category,
String? priority,
int? createdAt,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
completed: completed ?? this.completed,
category: category ?? this.category,
priority: priority ?? this.priority,
createdAt: createdAt ?? this.createdAt,
);
}
Map<String, dynamic> toMap() {
return {
'title': title,
'completed': completed ? 1 : 0,
'category': category,
'priority': priority,
};
}
factory Todo.fromMap(Map<String, dynamic> map) {
return Todo(
id: map['id'] as int?,
title: map['title'] as String,
completed: (map['completed'] as int?) == 1,
category: map['category'] as String? ?? 'Personal',
priority: map['priority'] as String? ?? 'medium',
createdAt: map['created_at'] as int?,
);
}
}
+32
View File
@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import '../widgets/filter_bar.dart';
import '../widgets/todo_list.dart';
import '../widgets/todo_form.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Todos Dart Flutter'),
centerTitle: false,
backgroundColor: colorScheme.primaryContainer,
foregroundColor: colorScheme.onPrimaryContainer,
),
body: const Column(
children: [
FilterBar(),
Expanded(child: TodoList()),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => showTodoDialog(context, null),
child: const Icon(Icons.add),
),
);
}
}
+88
View File
@@ -0,0 +1,88 @@
import 'package:flutter/foundation.dart';
import '../models/todo.dart';
import '../db.dart';
const List<String> categories = ['Personal', 'Work', 'Shopping', 'Health', 'Other'];
const List<String> priorityKeys = ['low', 'medium', 'high'];
const Map<String, int> priorityColors = {
'low': 0xFF4CAF50,
'medium': 0xFFFF9800,
'high': 0xFFF44336,
};
enum TodoFilter { all, active, completed }
class TodoState extends ChangeNotifier {
List<Todo> _todos = [];
TodoFilter _filter = TodoFilter.all;
List<Todo> get todos => _todos;
TodoFilter get filter => _filter;
List<Todo> get filteredTodos {
switch (_filter) {
case TodoFilter.active:
return _todos.where((t) => !t.completed).toList();
case TodoFilter.completed:
return _todos.where((t) => t.completed).toList();
case TodoFilter.all:
return _todos;
}
}
Map<String, int> get counts => {
'all': _todos.length,
'active': _todos.where((t) => !t.completed).length,
'completed': _todos.where((t) => t.completed).length,
};
void setTodos(List<Todo> todos) {
_todos = todos;
notifyListeners();
}
void setFilter(TodoFilter f) {
_filter = f;
notifyListeners();
}
Future<void> addTodo(String title, String category, String priority) async {
final id = await TodoDatabase.insertTodo(title, category, priority);
_todos.insert(0, Todo(
id: id,
title: title,
category: category,
priority: priority,
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
));
notifyListeners();
}
Future<void> toggleTodo(int id) async {
final idx = _todos.indexWhere((t) => t.id == id);
if (idx == -1) return;
final todo = _todos[idx];
final newCompleted = !todo.completed;
_todos[idx] = todo.copyWith(completed: newCompleted);
notifyListeners();
await TodoDatabase.toggleTodo(id, newCompleted);
}
Future<void> updateTodo(int id, String title, String category, String priority) async {
final idx = _todos.indexWhere((t) => t.id == id);
if (idx == -1) return;
_todos[idx] = _todos[idx].copyWith(
title: title,
category: category,
priority: priority,
);
notifyListeners();
await TodoDatabase.updateTodo(id, title, category, priority);
}
Future<void> deleteTodo(int id) async {
_todos.removeWhere((t) => t.id == id);
notifyListeners();
await TodoDatabase.deleteTodo(id);
}
}
+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),
);
},
);
}
}