Add todos admin and 30s refresh
This commit is contained in:
@@ -13,7 +13,8 @@ class AnnouncementWidget extends StatefulWidget {
|
||||
|
||||
class _AnnouncementWidgetState extends State<AnnouncementWidget> {
|
||||
final PageController _pageController = PageController();
|
||||
Timer? _timer;
|
||||
Timer? _scrollTimer;
|
||||
Timer? _refreshTimer;
|
||||
int _currentPage = 0;
|
||||
List<Announcement> _announcements = [];
|
||||
|
||||
@@ -21,6 +22,13 @@ class _AnnouncementWidgetState extends State<AnnouncementWidget> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchAnnouncements();
|
||||
_startAutoRefresh();
|
||||
}
|
||||
|
||||
void _startAutoRefresh() {
|
||||
_refreshTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
||||
_fetchAnnouncements();
|
||||
});
|
||||
}
|
||||
|
||||
void _fetchAnnouncements() async {
|
||||
@@ -41,9 +49,9 @@ class _AnnouncementWidgetState extends State<AnnouncementWidget> {
|
||||
}
|
||||
|
||||
void _startAutoScroll() {
|
||||
_timer?.cancel();
|
||||
_scrollTimer?.cancel();
|
||||
if (_announcements.length > 1) {
|
||||
_timer = Timer.periodic(const Duration(seconds: 10), (timer) {
|
||||
_scrollTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
|
||||
if (_pageController.hasClients) {
|
||||
_currentPage++;
|
||||
if (_currentPage >= _announcements.length) {
|
||||
@@ -61,7 +69,8 @@ class _AnnouncementWidgetState extends State<AnnouncementWidget> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_scrollTimer?.cancel();
|
||||
_refreshTimer?.cancel();
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,26 @@ import 'package:provider/provider.dart';
|
||||
import '../models/bible_verse.dart';
|
||||
import '../services/bible_service.dart';
|
||||
|
||||
class BibleVerseWidget extends StatelessWidget {
|
||||
class BibleVerseWidget extends StatefulWidget {
|
||||
const BibleVerseWidget({super.key});
|
||||
|
||||
@override
|
||||
State<BibleVerseWidget> createState() => _BibleVerseWidgetState();
|
||||
}
|
||||
|
||||
class _BibleVerseWidgetState extends State<BibleVerseWidget> {
|
||||
late Future<BibleVerse> _verseFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Fetch only once on init
|
||||
_verseFuture = Provider.of<BibleService>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchTodayVerse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -24,10 +41,7 @@ class BibleVerseWidget extends StatelessWidget {
|
||||
),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: FutureBuilder<BibleVerse>(
|
||||
future: Provider.of<BibleService>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchTodayVerse(),
|
||||
future: _verseFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
|
||||
@@ -1,12 +1,66 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/schedule_item.dart';
|
||||
import '../services/schedule_service.dart';
|
||||
|
||||
class ScheduleListWidget extends StatelessWidget {
|
||||
class ScheduleListWidget extends StatefulWidget {
|
||||
const ScheduleListWidget({super.key});
|
||||
|
||||
@override
|
||||
State<ScheduleListWidget> createState() => _ScheduleListWidgetState();
|
||||
}
|
||||
|
||||
class _ScheduleListWidgetState extends State<ScheduleListWidget> {
|
||||
Timer? _timer;
|
||||
List<ScheduleItem> _schedules = [];
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchSchedules();
|
||||
_startAutoRefresh();
|
||||
}
|
||||
|
||||
void _startAutoRefresh() {
|
||||
_timer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
||||
_fetchSchedules();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _fetchSchedules() async {
|
||||
try {
|
||||
final data = await Provider.of<ScheduleService>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchWeeklySchedules();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_schedules = data;
|
||||
_isLoading = false;
|
||||
_error = null;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = 'Failed to load schedules';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -21,109 +75,102 @@ class ScheduleListWidget extends StatelessWidget {
|
||||
Text(
|
||||
'Weekly Schedule',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<ScheduleItem>>(
|
||||
future: Provider.of<ScheduleService>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchWeeklySchedules(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Failed to load schedules',
|
||||
style: TextStyle(color: Colors.white54),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'No schedules this week',
|
||||
style: TextStyle(color: Colors.white54),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final schedules = snapshot.data!;
|
||||
// Sort by date
|
||||
schedules.sort((a, b) => a.startDate.compareTo(b.startDate));
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: schedules.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(color: Colors.white10),
|
||||
itemBuilder: (context, index) {
|
||||
final item = schedules[index];
|
||||
final dateStr = DateFormat(
|
||||
'E, MMM d',
|
||||
).format(item.startDate);
|
||||
final timeStr = item.isAllDay
|
||||
? 'All Day'
|
||||
: DateFormat('HH:mm').format(item.startDate);
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white10,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('d').format(item.startDate),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('E').format(item.startDate),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
item.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
timeStr,
|
||||
style: const TextStyle(color: Colors.white54),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
child: _buildContent(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
if (_isLoading && _schedules.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (_error != null && _schedules.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_error!,
|
||||
style: const TextStyle(color: Colors.white54),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_schedules.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'No schedules this week',
|
||||
style: TextStyle(color: Colors.white54),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Sort by date
|
||||
final sortedSchedules = List<ScheduleItem>.from(_schedules);
|
||||
sortedSchedules.sort((a, b) => a.startDate.compareTo(b.startDate));
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: sortedSchedules.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(color: Colors.white10),
|
||||
itemBuilder: (context, index) {
|
||||
final item = sortedSchedules[index];
|
||||
final timeStr = item.isAllDay
|
||||
? 'All Day'
|
||||
: DateFormat('HH:mm').format(item.startDate);
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white10,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('d').format(item.startDate),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('E').format(item.startDate),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
item.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
timeStr,
|
||||
style: const TextStyle(color: Colors.white54),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/todo_item.dart';
|
||||
@@ -5,9 +6,70 @@ import '../models/family_member.dart';
|
||||
import '../services/todo_service.dart';
|
||||
import '../services/family_service.dart';
|
||||
|
||||
class TodoListWidget extends StatelessWidget {
|
||||
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(
|
||||
@@ -37,176 +99,158 @@ class TodoListWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<dynamic>>(
|
||||
future: Future.wait([
|
||||
Provider.of<TodoService>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchTodayTodos(),
|
||||
Provider.of<FamilyService>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchFamilyMembers(),
|
||||
]),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Failed to load todos',
|
||||
style: TextStyle(color: Colors.white54),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'No todos today',
|
||||
style: TextStyle(color: Colors.white54),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final todos = snapshot.data![0] as List<TodoItem>;
|
||||
final members = snapshot.data![1] as List<FamilyMember>;
|
||||
|
||||
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(
|
||||
'All done for today!',
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
);
|
||||
await Provider.of<TodoService>(
|
||||
context,
|
||||
listen: false,
|
||||
).updateTodo(updated);
|
||||
// Force rebuild? In a real app we'd use a reactive state.
|
||||
// Here we rely on the parent or timer to refresh, or we could convert this to StatefulWidget.
|
||||
// For now, let's just let the next refresh cycle pick it up, or if the user interacts, maybe we should optimistic update?
|
||||
// Given it's a TV dashboard, interaction might be rare, but if it is interactive:
|
||||
(context as Element)
|
||||
.markNeedsBuild(); // HACK to refresh
|
||||
},
|
||||
activeColor: Theme.of(context).colorScheme.secondary,
|
||||
checkColor: Colors.black,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
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(
|
||||
'All done for today!',
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user