Files
bini-google-tv/flutter_app/lib/widgets/todo_list_widget.dart

264 lines
7.4 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../models/todo_item.dart';
import '../models/family_member.dart';
import '../services/todo_service.dart';
import '../services/family_service.dart';
class TodoListWidget extends StatefulWidget {
const TodoListWidget({super.key});
@override
State<TodoListWidget> createState() => _TodoListWidgetState();
}
class _TodoListWidgetState extends State<TodoListWidget> {
Timer? _timer;
List<TodoItem> _todos = [];
List<FamilyMember> _members = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_fetchData();
_startAutoRefresh();
}
void _startAutoRefresh() {
_timer = Timer.periodic(const Duration(seconds: 30), (timer) {
_fetchData();
});
}
Future<void> _fetchData() async {
try {
final results = await Future.wait([
Provider.of<TodoService>(
context,
listen: false,
).fetchTodayTodos(),
Provider.of<FamilyService>(
context,
listen: false,
).fetchFamilyMembers(),
]);
if (mounted) {
setState(() {
_todos = results[0] as List<TodoItem>;
_members = results[1] as List<FamilyMember>;
_isLoading = false;
_error = null;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = 'Failed to load todos';
_isLoading = false;
});
}
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"오늘의 할 일",
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Icon(
Icons.check_circle_outline,
color: Theme.of(context).colorScheme.secondary,
),
],
),
const SizedBox(height: 12),
Expanded(
child: _buildContent(),
),
],
),
);
}
Widget _buildContent() {
if (_isLoading && _todos.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null && _todos.isEmpty) {
return Center(
child: Text(
_error!,
style: const TextStyle(color: Colors.white54),
),
);
}
if (_todos.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.thumb_up, color: Colors.white24, size: 32),
SizedBox(height: 8),
Text(
'오늘 할 일을 모두 마쳤습니다!',
style: TextStyle(color: Colors.white54),
),
],
),
);
}
return ListView.separated(
itemCount: _todos.length,
separatorBuilder: (context, index) =>
const Divider(color: Colors.white10),
itemBuilder: (context, index) {
final todo = _todos[index];
final member = _members.firstWhere(
(m) => m.id == todo.familyMemberId,
orElse: () => const FamilyMember(
id: '',
name: 'Unknown',
iconUrl: '',
emoji: '👤',
color: '#888888',
order: 0,
),
);
// Parse color
Color memberColor;
try {
String cleanHex =
member.color.replaceAll('#', '').replaceAll('0x', '');
if (cleanHex.length == 6) {
cleanHex = 'FF$cleanHex';
}
if (cleanHex.length == 8) {
memberColor = Color(int.parse('0x$cleanHex'));
} else {
memberColor = Colors.grey;
}
} catch (_) {
memberColor = Colors.grey;
}
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
backgroundColor: memberColor.withOpacity(0.2),
child: member.iconUrl.isNotEmpty
? ClipOval(
child: Image.network(
member.iconUrl,
width: 40,
height: 40,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Text(
member.name.isNotEmpty
? member.name[0].toUpperCase()
: '?',
style: TextStyle(
color: memberColor,
fontWeight: FontWeight.bold,
fontSize: 20,
),
);
},
),
)
: (member.name.isNotEmpty
? Text(
member.name[0].toUpperCase(),
style: TextStyle(
color: memberColor,
fontWeight: FontWeight.bold,
fontSize: 20,
),
)
: Icon(
Icons.person,
color: memberColor,
)),
),
title: Text(
todo.title,
style: TextStyle(
color: todo.completed ? Colors.white54 : Colors.white,
decoration: todo.completed ? TextDecoration.lineThrough : null,
decorationColor: Colors.white54,
),
),
subtitle: todo.dueDate != null && (todo.dueDate!.hour != 0 || todo.dueDate!.minute != 0)
? Text(
DateFormat('HH:mm').format(todo.dueDate!),
style: const TextStyle(color: Colors.white54, fontSize: 12),
)
: null,
trailing: Checkbox(
value: todo.completed,
onChanged: (val) async {
// Toggle completion
final updated = TodoItem(
id: todo.id,
familyMemberId: todo.familyMemberId,
title: todo.title,
completed: val ?? false,
dueDate: todo.dueDate,
);
// Optimistic update
setState(() {
final idx = _todos.indexWhere((t) => t.id == todo.id);
if (idx != -1) {
_todos[idx] = updated;
}
});
await Provider.of<TodoService>(
context,
listen: false,
).updateTodo(updated);
// Refresh to ensure sync
_fetchData();
},
activeColor: Theme.of(context).colorScheme.secondary,
checkColor: Colors.black,
),
);
},
);
}
}