mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
feat: Add settings provider for theme and time range management
- Implemented SettingsProvider to manage user preferences for theme options and time ranges for reminders. - Added persistent reminder settings with configurable retry intervals and maximum attempts. - Created UI for settings screen to allow users to customize their preferences. - Integrated shared_preferences for persistent storage of user settings. feat: Introduce Ingredient model - Created Ingredient model to represent nutritional components with properties for id, name, amount, and unit. - Added methods for serialization and deserialization of Ingredient objects. feat: Develop Archived Supplements Screen - Implemented ArchivedSupplementsScreen to display archived supplements with options to unarchive or delete. - Added UI components for listing archived supplements and handling user interactions. chore: Update dependencies in pubspec.yaml and pubspec.lock - Updated shared_preferences dependency to the latest version. - Removed flutter_datetime_picker_plus dependency and added file dependency. - Updated Flutter SDK constraint to >=3.27.0.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../providers/supplement_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../models/supplement.dart';
|
||||
import '../widgets/supplement_card.dart';
|
||||
import 'add_supplement_screen.dart';
|
||||
import 'archived_supplements_screen.dart';
|
||||
|
||||
class SupplementsListScreen extends StatelessWidget {
|
||||
const SupplementsListScreen({super.key});
|
||||
@@ -15,9 +16,22 @@ class SupplementsListScreen extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
title: const Text('My Supplements'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.archive),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ArchivedSupplementsScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: 'Archived Supplements',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Consumer<SupplementProvider>(
|
||||
builder: (context, provider, child) {
|
||||
body: Consumer2<SupplementProvider, SettingsProvider>(
|
||||
builder: (context, provider, settingsProvider, child) {
|
||||
if (provider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
@@ -56,86 +70,190 @@ class SupplementsListScreen extends StatelessWidget {
|
||||
onRefresh: () async {
|
||||
await provider.loadSupplements();
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
// Today's Intakes Section
|
||||
if (provider.todayIntakes.isNotEmpty) ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Today\'s Intakes',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...provider.todayIntakes.map((intake) {
|
||||
final takenAt = DateTime.parse(intake['takenAt']);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
'${intake['supplementName']} - ${intake['dosageTaken']} ${intake['supplementUnit']} at ${DateFormat('HH:mm').format(takenAt)}',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
// Supplements List
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: provider.supplements.length,
|
||||
itemBuilder: (context, index) {
|
||||
final supplement = provider.supplements[index];
|
||||
return SupplementCard(
|
||||
supplement: supplement,
|
||||
onTake: () => _showTakeDialog(context, supplement),
|
||||
onEdit: () => _editSupplement(context, supplement),
|
||||
onDelete: () => _deleteSupplement(context, supplement),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _buildGroupedSupplementsList(context, provider.supplements, settingsProvider),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGroupedSupplementsList(BuildContext context, List<Supplement> supplements, SettingsProvider settingsProvider) {
|
||||
final groupedSupplements = _groupSupplementsByTimeOfDay(supplements, settingsProvider);
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (groupedSupplements['morning']!.isNotEmpty) ...[
|
||||
_buildSectionHeader('Morning (${settingsProvider.morningRange})', Icons.wb_sunny, Colors.orange, groupedSupplements['morning']!.length),
|
||||
...groupedSupplements['morning']!.map((supplement) =>
|
||||
SupplementCard(
|
||||
supplement: supplement,
|
||||
onTake: () => _showTakeDialog(context, supplement),
|
||||
onEdit: () => _editSupplement(context, supplement),
|
||||
onDelete: () => _deleteSupplement(context, supplement),
|
||||
onArchive: () => _archiveSupplement(context, supplement),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
if (groupedSupplements['afternoon']!.isNotEmpty) ...[
|
||||
_buildSectionHeader('Afternoon (${settingsProvider.afternoonRange})', Icons.light_mode, Colors.blue, groupedSupplements['afternoon']!.length),
|
||||
...groupedSupplements['afternoon']!.map((supplement) =>
|
||||
SupplementCard(
|
||||
supplement: supplement,
|
||||
onTake: () => _showTakeDialog(context, supplement),
|
||||
onEdit: () => _editSupplement(context, supplement),
|
||||
onDelete: () => _deleteSupplement(context, supplement),
|
||||
onArchive: () => _archiveSupplement(context, supplement),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
if (groupedSupplements['evening']!.isNotEmpty) ...[
|
||||
_buildSectionHeader('Evening (${settingsProvider.eveningRange})', Icons.nightlight_round, Colors.indigo, groupedSupplements['evening']!.length),
|
||||
...groupedSupplements['evening']!.map((supplement) =>
|
||||
SupplementCard(
|
||||
supplement: supplement,
|
||||
onTake: () => _showTakeDialog(context, supplement),
|
||||
onEdit: () => _editSupplement(context, supplement),
|
||||
onDelete: () => _deleteSupplement(context, supplement),
|
||||
onArchive: () => _archiveSupplement(context, supplement),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
if (groupedSupplements['night']!.isNotEmpty) ...[
|
||||
_buildSectionHeader('Night (${settingsProvider.nightRange})', Icons.bedtime, Colors.purple, groupedSupplements['night']!.length),
|
||||
...groupedSupplements['night']!.map((supplement) =>
|
||||
SupplementCard(
|
||||
supplement: supplement,
|
||||
onTake: () => _showTakeDialog(context, supplement),
|
||||
onEdit: () => _editSupplement(context, supplement),
|
||||
onDelete: () => _deleteSupplement(context, supplement),
|
||||
onArchive: () => _archiveSupplement(context, supplement),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
if (groupedSupplements['anytime']!.isNotEmpty) ...[
|
||||
_buildSectionHeader('Anytime', Icons.schedule, Colors.grey, groupedSupplements['anytime']!.length),
|
||||
...groupedSupplements['anytime']!.map((supplement) =>
|
||||
SupplementCard(
|
||||
supplement: supplement,
|
||||
onTake: () => _showTakeDialog(context, supplement),
|
||||
onEdit: () => _editSupplement(context, supplement),
|
||||
onDelete: () => _deleteSupplement(context, supplement),
|
||||
onArchive: () => _archiveSupplement(context, supplement),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title, IconData icon, Color color, int count) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title.contains('(') ? title.split('(')[0].trim() : title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
if (title.contains('(')) ...[
|
||||
Text(
|
||||
'(${title.split('(')[1]}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: color.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, List<Supplement>> _groupSupplementsByTimeOfDay(List<Supplement> supplements, SettingsProvider settingsProvider) {
|
||||
final Map<String, List<Supplement>> grouped = {
|
||||
'morning': <Supplement>[],
|
||||
'afternoon': <Supplement>[],
|
||||
'evening': <Supplement>[],
|
||||
'night': <Supplement>[],
|
||||
'anytime': <Supplement>[],
|
||||
};
|
||||
|
||||
for (final supplement in supplements) {
|
||||
final category = settingsProvider.determineTimeCategory(supplement.reminderTimes);
|
||||
grouped[category]!.add(supplement);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
void _showTakeDialog(BuildContext context, Supplement supplement) {
|
||||
final unitsController = TextEditingController(text: supplement.numberOfUnits.toString());
|
||||
final notesController = TextEditingController();
|
||||
DateTime selectedDateTime = DateTime.now();
|
||||
bool useCustomTime = false;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
final units = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble();
|
||||
final totalDosage = supplement.dosageAmount * units;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('Take ${supplement.name}'),
|
||||
content: Column(
|
||||
@@ -175,7 +293,7 @@ class SupplementsListScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${totalDosage.toStringAsFixed(totalDosage % 1 == 0 ? 0 : 1)} ${supplement.unit}',
|
||||
supplement.ingredientsDisplay,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -186,6 +304,162 @@ class SupplementsListScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Time selection section
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'When did you take it?',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RadioListTile<bool>(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Just now', style: TextStyle(fontSize: 12)),
|
||||
value: false,
|
||||
groupValue: useCustomTime,
|
||||
onChanged: (value) => setState(() => useCustomTime = value!),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RadioListTile<bool>(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Custom time', style: TextStyle(fontSize: 12)),
|
||||
value: true,
|
||||
groupValue: useCustomTime,
|
||||
onChanged: (value) => setState(() => useCustomTime = value!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (useCustomTime) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Date picker
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Date: ${selectedDateTime.day}/${selectedDateTime.month}/${selectedDateTime.year}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDateTime,
|
||||
firstDate: DateTime.now().subtract(const Duration(days: 7)),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
selectedDateTime = DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
selectedDateTime.hour,
|
||||
selectedDateTime.minute,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
child: const Text('Change', style: TextStyle(fontSize: 10)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Time picker
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Time: ${selectedDateTime.hour.toString().padLeft(2, '0')}:${selectedDateTime.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(selectedDateTime),
|
||||
);
|
||||
if (time != null) {
|
||||
setState(() {
|
||||
selectedDateTime = DateTime(
|
||||
selectedDateTime.year,
|
||||
selectedDateTime.month,
|
||||
selectedDateTime.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
child: const Text('Change', style: TextStyle(fontSize: 10)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: notesController,
|
||||
decoration: const InputDecoration(
|
||||
@@ -204,12 +478,15 @@ class SupplementsListScreen extends StatelessWidget {
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final unitsTaken = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble();
|
||||
final totalDosageTaken = supplement.dosageAmount * unitsTaken;
|
||||
// For now, we'll record 0 as total dosage since we're transitioning to ingredients
|
||||
// This will be properly implemented when we add the full ingredient tracking
|
||||
final totalDosageTaken = 0.0;
|
||||
context.read<SupplementProvider>().recordIntake(
|
||||
supplement.id!,
|
||||
totalDosageTaken,
|
||||
unitsTaken: unitsTaken,
|
||||
notes: notesController.text.isNotEmpty ? notesController.text : null,
|
||||
takenAt: useCustomTime ? selectedDateTime : null,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -219,7 +496,7 @@ class SupplementsListScreen extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Take'),
|
||||
child: const Text('Record'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -265,4 +542,34 @@ class SupplementsListScreen extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _archiveSupplement(BuildContext context, Supplement supplement) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Archive Supplement'),
|
||||
content: Text('Are you sure you want to archive ${supplement.name}? You can unarchive it later from the archived supplements list.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<SupplementProvider>().archiveSupplement(supplement.id!);
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${supplement.name} archived'),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
|
||||
child: const Text('Archive'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user