feat: Update theme colors and add Today Schedule screen

- Changed color scheme from ShadBlueColorScheme to ShadZincColorScheme in main.dart
- Added getMostRecentIntake method in SupplementProvider to retrieve the latest intake for a supplement
- Integrated TodayScheduleScreen into HomeScreen with a new BottomNavigationBar item
- Updated SupplementsListScreen title and adjusted layout for better UX
- Enhanced SupplementCard to support undoing last taken action and improved popover menu options
- Added popover package for better UI interactions
This commit is contained in:
2025-08-31 20:00:32 +02:00
parent 666008f05d
commit 7c63eb473b
8 changed files with 750 additions and 397 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:popover/popover.dart';
import '../models/supplement.dart';
import '../providers/supplement_provider.dart';
@@ -11,6 +12,8 @@ class SupplementCard extends StatefulWidget {
final VoidCallback onDelete;
final VoidCallback onArchive;
final VoidCallback onDuplicate;
final VoidCallback? onUndoLastTaken;
final bool showCompletionStatus;
const SupplementCard({
super.key,
@@ -20,6 +23,8 @@ class SupplementCard extends StatefulWidget {
required this.onDelete,
required this.onArchive,
required this.onDuplicate,
this.onUndoLastTaken,
this.showCompletionStatus = true,
});
@override
@@ -29,16 +34,48 @@ class SupplementCard extends StatefulWidget {
class _SupplementCardState extends State<SupplementCard> {
bool _isExpanded = false;
Widget _buildPopoverItem({
required IconData icon,
required String label,
required VoidCallback onTap,
Color? color,
}) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
icon,
size: 18,
color: color ?? Theme.of(context).colorScheme.onSurface,
),
const SizedBox(width: 12),
Text(
label,
style: TextStyle(
fontSize: 14,
color: color ?? Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Consumer<SupplementProvider>(
builder: (context, provider, child) {
final bool isTakenToday = provider.hasBeenTakenToday(widget.supplement.id!);
final int todayIntakeCount = provider.getTodayIntakeCount(widget.supplement.id!);
final bool isCompletelyTaken = todayIntakeCount >= widget.supplement.frequencyPerDay;
final bool isTakenToday = widget.showCompletionStatus ? provider.hasBeenTakenToday(widget.supplement.id!) : false;
final int todayIntakeCount = widget.showCompletionStatus ? provider.getTodayIntakeCount(widget.supplement.id!) : 0;
final bool isCompletelyTaken = widget.showCompletionStatus ? todayIntakeCount >= widget.supplement.frequencyPerDay : false;
// Get today's intake times for this supplement
final todayIntakes = provider.todayIntakes
// Get today's intake times for this supplement (only if showing completion status)
final todayIntakes = widget.showCompletionStatus ? provider.todayIntakes
.where((intake) => intake['supplement_id'] == widget.supplement.id)
.map((intake) {
final takenAt = DateTime.parse(intake['takenAt']);
@@ -47,410 +84,227 @@ class _SupplementCardState extends State<SupplementCard> {
'time': '${takenAt.hour.toString().padLeft(2, '0')}:${takenAt.minute.toString().padLeft(2, '0')}',
'units': unitsTaken is int ? unitsTaken.toDouble() : unitsTaken as double,
};
}).toList();
}).toList() : [];
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: isCompletelyTaken
? Theme.of(context).colorScheme.surface
: isTakenToday
? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.surface,
border: Border.all(
color: isCompletelyTaken
? Colors.green.shade600
: isTakenToday
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2),
width: 1.5,
),
),
child: Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
initiallyExpanded: _isExpanded,
onExpansionChanged: (expanded) {
setState(() {
_isExpanded = expanded;
});
},
tilePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
childrenPadding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
leading: Container(
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.only(bottom: 8),
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: InkWell(
onTap: () {
showPopover(
context: context,
bodyBuilder: (context) => Container(
constraints: const BoxConstraints(maxWidth: 200),
decoration: BoxDecoration(
color: isCompletelyTaken
? Colors.green.shade500
: isTakenToday
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
isCompletelyTaken
? Icons.check_circle
: isTakenToday
? Icons.schedule
: Icons.medication,
color: isCompletelyTaken
? Theme.of(context).colorScheme.inverseSurface
: isTakenToday
? Theme.of(context).colorScheme.onSecondary
: Theme.of(context).colorScheme.primary,
size: 20,
),
),
title: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.supplement.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isCompletelyTaken
? Theme.of(context).colorScheme.inverseSurface
: isTakenToday
? Theme.of(context).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface,
),
),
if (widget.supplement.brand != null && widget.supplement.brand!.isNotEmpty)
Text(
widget.supplement.brand!,
style: TextStyle(
fontSize: 12,
color: isCompletelyTaken
? Colors.green.shade200
: isTakenToday
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
],
),
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3),
width: 1,
),
// Status badge and take button in collapsed view
if (!_isExpanded) ...[
if (isCompletelyTaken)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.shade500,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Complete',
style: TextStyle(
color: Theme.of(context).colorScheme.inverseSurface,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
)
else ...[
if (isTakenToday)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'$todayIntakeCount/${widget.supplement.frequencyPerDay}',
style: TextStyle(
color: Theme.of(context).colorScheme.onSecondary,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.shadow.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
],
),
trailing: PopupMenuButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.more_vert,
color: isCompletelyTaken
? Theme.of(context).colorScheme.inverseSurface
: Theme.of(context).colorScheme.onSurfaceVariant,
),
onSelected: (value) {
switch (value) {
case 'edit':
widget.onEdit();
break;
case 'duplicate':
widget.onDuplicate();
break;
case 'archive':
widget.onArchive();
break;
case 'delete':
widget.onDelete();
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'take',
onTap: widget.onTake,
child: Row(
children: [
Icon(isTakenToday ? Icons.add_circle_outline : Icons.medication),
const SizedBox(width: 8),
Text(isTakenToday ? 'Take Again' : 'Take'),
],
),
),
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit),
SizedBox(width: 8),
Text('Edit'),
],
),
),
const PopupMenuItem(
value: 'duplicate',
child: Row(
children: [
Icon(Icons.copy),
SizedBox(width: 8),
Text('Duplicate'),
],
),
),
const PopupMenuItem(
value: 'archive',
child: Row(
children: [
Icon(Icons.archive, color: Colors.orange),
SizedBox(width: 8),
Text('Archive'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Delete', style: TextStyle(color: Colors.red)),
],
),
),
],
),
children: [
// Today's intake times (if any) - only show in expanded view
if (todayIntakes.isNotEmpty) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isCompletelyTaken
? Colors.green.shade700.withValues(alpha: 0.8)
: Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isCompletelyTaken
? Colors.green.shade500
: Theme.of(context).colorScheme.secondary,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.onUndoLastTaken != null && isTakenToday)
_buildPopoverItem(
icon: Icons.undo,
label: 'Undo Last Taken',
onTap: () {
Navigator.of(context).pop();
widget.onUndoLastTaken!();
},
color: Colors.orange,
),
_buildPopoverItem(
icon: Icons.edit,
label: 'Edit',
onTap: () {
Navigator.of(context).pop();
widget.onEdit();
},
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.check_circle_outline,
size: 16,
color: isCompletelyTaken
? Colors.green.shade200
: Theme.of(context).colorScheme.onSecondaryContainer,
),
const SizedBox(width: 6),
Text(
'Taken today:',
_buildPopoverItem(
icon: Icons.copy,
label: 'Duplicate',
onTap: () {
Navigator.of(context).pop();
widget.onDuplicate();
},
),
_buildPopoverItem(
icon: Icons.archive,
label: 'Archive',
color: Colors.orange,
onTap: () {
Navigator.of(context).pop();
widget.onArchive();
},
),
_buildPopoverItem(
icon: Icons.delete,
label: 'Delete',
color: Colors.red,
onTap: () {
Navigator.of(context).pop();
widget.onDelete();
},
),
],
),
),
direction: PopoverDirection.bottom,
width: 180,
height: null,
arrowHeight: 0,
arrowWidth: 0,
backgroundColor: Colors.transparent,
shadow: const [],
);
},
borderRadius: BorderRadius.circular(12),
splashColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.4),
highlightColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: widget.showCompletionStatus ? (isCompletelyTaken
? Theme.of(context).colorScheme.surface
: isTakenToday
? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.surface) : Theme.of(context).colorScheme.surface,
border: Border.all(
color: widget.showCompletionStatus ? (isCompletelyTaken
? Colors.green.shade600
: isTakenToday
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2)) : Theme.of(context).colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: widget.showCompletionStatus ? (isCompletelyTaken
? Colors.green.shade500
: isTakenToday
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)) : Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
widget.showCompletionStatus ? (isCompletelyTaken
? Icons.check_circle
: isTakenToday
? Icons.schedule
: Icons.medication) : Icons.medication,
color: widget.showCompletionStatus ? (isCompletelyTaken
? Theme.of(context).colorScheme.inverseSurface
: isTakenToday
? Theme.of(context).colorScheme.onSecondary
: Theme.of(context).colorScheme.primary) : Theme.of(context).colorScheme.primary,
size: 18,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
widget.supplement.name,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isCompletelyTaken
? Colors.green.shade200
: Theme.of(context).colorScheme.onSecondaryContainer,
fontSize: 14,
fontWeight: FontWeight.bold,
color: widget.showCompletionStatus ? (isCompletelyTaken
? Theme.of(context).colorScheme.inverseSurface
: isTakenToday
? Theme.of(context).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onSurface) : Theme.of(context).colorScheme.onSurface,
),
),
),
if (widget.showCompletionStatus && isCompletelyTaken) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.shade500,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Complete',
style: TextStyle(
color: Theme.of(context).colorScheme.inverseSurface,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
if (widget.supplement.brand != null && widget.supplement.brand!.isNotEmpty)
Text(
widget.supplement.brand!,
style: TextStyle(
fontSize: 11,
color: widget.showCompletionStatus ? (isCompletelyTaken
? Colors.green.shade200
: isTakenToday
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary) : Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Wrap(
spacing: 8,
runSpacing: 4,
children: todayIntakes.map((intake) {
final units = intake['units'] as double;
final unitsText = units == 1.0
? widget.supplement.unitType
: '${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${widget.supplement.unitType}';
return Container(
const SizedBox(height: 4),
Row(
children: [
Text(
'${widget.supplement.frequencyPerDay}x daily • ${widget.supplement.numberOfUnits} ${widget.supplement.unitType}',
style: TextStyle(
fontSize: 11,
color: ShadTheme.of(context).colorScheme.foreground.withValues(alpha: 0.7),
),
),
if (widget.showCompletionStatus && isTakenToday && !isCompletelyTaken) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isCompletelyTaken
? Colors.green.shade600
: Theme.of(context).colorScheme.secondary,
borderRadius: BorderRadius.circular(6),
color: Theme.of(context).colorScheme.secondary,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${intake['time']}$unitsText',
'$todayIntakeCount/${widget.supplement.frequencyPerDay}',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: isCompletelyTaken
? Colors.white
: Theme.of(context).colorScheme.onSecondary,
color: Theme.of(context).colorScheme.onSecondary,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
);
}).toList(),
),
],
),
),
const SizedBox(height: 16),
],
// Ingredients section
ShadCard(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ingredients per ${widget.supplement.unitType}:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: ShadTheme.of(context).colorScheme.foreground,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 4,
children: widget.supplement.ingredients.map((ingredient) {
return ShadBadge(
child: Text(
'${ingredient.name} ${ingredient.amount}${ingredient.unit}',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
);
}).toList(),
),
],
),
),
],
],
),
],
),
),
const SizedBox(height: 12),
// Schedule and dosage info
Row(
children: [
Expanded(
child: ShadBadge(
child: Row(
children: [
Icon(
Icons.schedule,
size: 14,
color: ShadTheme.of(context).colorScheme.foreground,
),
const SizedBox(width: 4),
Text('${widget.supplement.frequencyPerDay}x daily'),
],
),
),
),
const SizedBox(width: 8),
Expanded(
child: ShadBadge(
child: Row(
children: [
Icon(
Icons.medication,
size: 14,
color: ShadTheme.of(context).colorScheme.foreground,
),
const SizedBox(width: 4),
Text('${widget.supplement.numberOfUnits} ${widget.supplement.unitType}'),
],
),
),
),
],
),
if (widget.supplement.reminderTimes.isNotEmpty) ...[
const SizedBox(height: 8),
ShadBadge(
child: Row(
children: [
Icon(
Icons.notifications,
size: 14,
color: ShadTheme.of(context).colorScheme.foreground,
),
const SizedBox(width: 4),
Expanded(
child: Text(
'Reminders: ${widget.supplement.reminderTimes.join(', ')}',
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
if (widget.supplement.notes != null && widget.supplement.notes!.isNotEmpty) ...[
const SizedBox(height: 8),
ShadCard(
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
widget.supplement.notes!,
style: TextStyle(
fontSize: 12,
color: ShadTheme.of(context).colorScheme.foreground,
fontStyle: FontStyle.italic,
),
),
),
),
],
],
),
),
@@ -508,3 +362,4 @@ class _InfoChip extends StatelessWidget {
);
}
}