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,9 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_datetime_picker_plus/flutter_datetime_picker_plus.dart';
|
||||
import '../models/supplement.dart';
|
||||
import '../models/ingredient.dart';
|
||||
import '../providers/supplement_provider.dart';
|
||||
|
||||
// Helper class to manage ingredient text controllers
|
||||
class IngredientController {
|
||||
final TextEditingController nameController;
|
||||
final TextEditingController amountController;
|
||||
String selectedUnit;
|
||||
|
||||
IngredientController({
|
||||
String name = '',
|
||||
double amount = 0.0,
|
||||
this.selectedUnit = 'mg',
|
||||
}) : nameController = TextEditingController(text: name),
|
||||
amountController = TextEditingController(text: amount > 0 ? amount.toString() : '');
|
||||
|
||||
Ingredient toIngredient() {
|
||||
return Ingredient(
|
||||
name: nameController.text.trim(),
|
||||
amount: double.tryParse(amountController.text) ?? 0.0,
|
||||
unit: selectedUnit,
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
amountController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class AddSupplementScreen extends StatefulWidget {
|
||||
final Supplement? supplement;
|
||||
|
||||
@@ -16,11 +43,13 @@ class AddSupplementScreen extends StatefulWidget {
|
||||
class _AddSupplementScreenState extends State<AddSupplementScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _dosageAmountController = TextEditingController();
|
||||
final _brandController = TextEditingController();
|
||||
final _numberOfUnitsController = TextEditingController();
|
||||
final _notesController = TextEditingController();
|
||||
|
||||
String _selectedUnit = 'mg';
|
||||
// Multi-ingredient support with persistent controllers
|
||||
List<IngredientController> _ingredientControllers = [];
|
||||
|
||||
String _selectedUnitType = 'capsules';
|
||||
int _frequencyPerDay = 1;
|
||||
List<String> _reminderTimes = ['08:00'];
|
||||
@@ -28,26 +57,159 @@ class _AddSupplementScreenState extends State<AddSupplementScreen> {
|
||||
final List<String> _units = ['mg', 'g', 'μg', 'IU', 'ml'];
|
||||
final List<String> _unitTypes = ['capsules', 'tablets', 'softgels', 'drops', 'ml', 'scoops', 'gummies'];
|
||||
|
||||
@override
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.supplement != null) {
|
||||
_initializeWithExistingSupplement();
|
||||
} else {
|
||||
_numberOfUnitsController.text = '1'; // Default to 1 unit
|
||||
// Start with one empty ingredient
|
||||
_ingredientControllers.add(IngredientController());
|
||||
}
|
||||
}
|
||||
|
||||
void _addIngredient() {
|
||||
setState(() {
|
||||
_ingredientControllers.add(IngredientController());
|
||||
});
|
||||
}
|
||||
|
||||
void _removeIngredient(int index) {
|
||||
if (_ingredientControllers.length > 1) {
|
||||
setState(() {
|
||||
_ingredientControllers[index].dispose();
|
||||
_ingredientControllers.removeAt(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateIngredient(int index, String field, dynamic value) {
|
||||
if (index < _ingredientControllers.length) {
|
||||
setState(() {
|
||||
if (field == 'unit') {
|
||||
_ingredientControllers[index].selectedUnit = value as String;
|
||||
}
|
||||
// Note: name and amount are handled by the TextEditingControllers directly
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildIngredientRow(int index, IngredientController controller) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Ingredient ${index + 1}',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const Spacer(),
|
||||
if (_ingredientControllers.length > 1)
|
||||
IconButton(
|
||||
onPressed: () => _removeIngredient(index),
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
color: Colors.red,
|
||||
tooltip: 'Remove ingredient',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: controller.nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ingredient Name *',
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'e.g., Vitamin D3, Magnesium',
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Please enter ingredient name';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextFormField(
|
||||
controller: controller.amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Amount *',
|
||||
border: OutlineInputBorder(),
|
||||
hintText: '100',
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Enter amount';
|
||||
}
|
||||
if (double.tryParse(value) == null || double.parse(value) <= 0) {
|
||||
return 'Enter valid amount';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: controller.selectedUnit,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Unit',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: _units.map((unit) {
|
||||
return DropdownMenuItem(
|
||||
value: unit,
|
||||
child: Text(unit),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
_updateIngredient(index, 'unit', value);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _initializeWithExistingSupplement() {
|
||||
final supplement = widget.supplement!;
|
||||
_nameController.text = supplement.name;
|
||||
_dosageAmountController.text = supplement.dosageAmount.toString();
|
||||
_brandController.text = supplement.brand ?? '';
|
||||
_numberOfUnitsController.text = supplement.numberOfUnits.toString();
|
||||
_notesController.text = supplement.notes ?? '';
|
||||
_selectedUnit = supplement.unit;
|
||||
_selectedUnitType = supplement.unitType;
|
||||
_frequencyPerDay = supplement.frequencyPerDay;
|
||||
_reminderTimes = List.from(supplement.reminderTimes);
|
||||
|
||||
// Initialize ingredient controllers from existing ingredients
|
||||
_ingredientControllers.clear();
|
||||
if (supplement.ingredients.isEmpty) {
|
||||
// If no ingredients, start with one empty controller
|
||||
_ingredientControllers.add(IngredientController());
|
||||
} else {
|
||||
for (final ingredient in supplement.ingredients) {
|
||||
_ingredientControllers.add(IngredientController(
|
||||
name: ingredient.name,
|
||||
amount: ingredient.amount,
|
||||
selectedUnit: ingredient.unit,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -83,53 +245,36 @@ class _AddSupplementScreenState extends State<AddSupplementScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Dosage amount per unit
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextFormField(
|
||||
controller: _dosageAmountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Amount per unit *',
|
||||
border: OutlineInputBorder(),
|
||||
hintText: '187',
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Please enter amount per unit';
|
||||
}
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _selectedUnit,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Unit',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: _units.map((unit) {
|
||||
return DropdownMenuItem(
|
||||
value: unit,
|
||||
child: Text(unit),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedUnit = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
// Brand field
|
||||
TextFormField(
|
||||
controller: _brandController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Brand (Optional)',
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'e.g., Nature Made, NOW Foods',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Ingredients section
|
||||
Text(
|
||||
'Ingredients',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
..._ingredientControllers.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final controller = entry.value;
|
||||
return _buildIngredientRow(index, controller);
|
||||
}),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _addIngredient,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Ingredient'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -181,15 +326,41 @@ class _AddSupplementScreenState extends State<AddSupplementScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Total per intake: ${_dosageAmountController.text.isNotEmpty && _numberOfUnitsController.text.isNotEmpty ? (double.tryParse(_dosageAmountController.text) ?? 0) * (int.tryParse(_numberOfUnitsController.text) ?? 0) : 0} $_selectedUnit',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Show ingredients summary
|
||||
if (_ingredientControllers.isNotEmpty && _ingredientControllers.any((c) => c.nameController.text.isNotEmpty && (double.tryParse(c.amountController.text) ?? 0) > 0))
|
||||
Card(
|
||||
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Per ${_numberOfUnitsController.text.isNotEmpty ? _numberOfUnitsController.text : "1"} $_selectedUnitType:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
..._ingredientControllers.where((c) => c.nameController.text.isNotEmpty && (double.tryParse(c.amountController.text) ?? 0) > 0).map((controller) {
|
||||
final amount = double.tryParse(controller.amountController.text) ?? 0;
|
||||
final totalAmount = amount * (int.tryParse(_numberOfUnitsController.text) ?? 1);
|
||||
return Text(
|
||||
'${totalAmount.toStringAsFixed(totalAmount % 1 == 0 ? 0 : 1)}${controller.selectedUnit} ${controller.nameController.text}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Frequency per day
|
||||
@@ -232,30 +403,66 @@ class _AddSupplementScreenState extends State<AddSupplementScreen> {
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => _selectTime(index),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
time,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Icons.edit,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(time),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_reminderTimes.length > 1)
|
||||
IconButton(
|
||||
onPressed: () => _removeReminderTime(index),
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: IconButton(
|
||||
onPressed: () => _removeReminderTime(index),
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
color: Colors.red,
|
||||
tooltip: 'Remove reminder time',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (_reminderTimes.length < _frequencyPerDay)
|
||||
TextButton.icon(
|
||||
onPressed: _addReminderTime,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Reminder Time'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _addReminderTime,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Reminder Time'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -299,17 +506,35 @@ class _AddSupplementScreenState extends State<AddSupplementScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _selectTime(int index) {
|
||||
DatePicker.showTimePicker(
|
||||
context,
|
||||
showTitleActions: true,
|
||||
onConfirm: (time) {
|
||||
setState(() {
|
||||
_reminderTimes[index] = '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||
});
|
||||
void _selectTime(int index) async {
|
||||
// Parse current time or use default
|
||||
TimeOfDay currentTime = TimeOfDay(hour: 8, minute: 0);
|
||||
if (index < _reminderTimes.length) {
|
||||
final timeParts = _reminderTimes[index].split(':');
|
||||
if (timeParts.length >= 2) {
|
||||
currentTime = TimeOfDay(
|
||||
hour: int.tryParse(timeParts[0]) ?? 8,
|
||||
minute: int.tryParse(timeParts[1]) ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final TimeOfDay? picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: currentTime,
|
||||
builder: (context, child) {
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
currentTime: DateTime.now(),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_reminderTimes[index] = '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _addReminderTime() {
|
||||
@@ -330,12 +555,31 @@ class _AddSupplementScreenState extends State<AddSupplementScreen> {
|
||||
|
||||
void _saveSupplement() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Validate that we have at least one ingredient with name and amount
|
||||
final validIngredients = _ingredientControllers.where((controller) =>
|
||||
controller.nameController.text.trim().isNotEmpty &&
|
||||
(double.tryParse(controller.amountController.text) ?? 0) > 0
|
||||
).map((controller) => Ingredient(
|
||||
name: controller.nameController.text.trim(),
|
||||
amount: double.tryParse(controller.amountController.text) ?? 0,
|
||||
unit: controller.selectedUnit,
|
||||
)).toList();
|
||||
|
||||
if (validIngredients.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please add at least one ingredient with name and amount'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final supplement = Supplement(
|
||||
id: widget.supplement?.id,
|
||||
name: _nameController.text.trim(),
|
||||
dosageAmount: double.parse(_dosageAmountController.text),
|
||||
brand: _brandController.text.trim().isNotEmpty ? _brandController.text.trim() : null,
|
||||
ingredients: validIngredients,
|
||||
numberOfUnits: int.parse(_numberOfUnitsController.text),
|
||||
unit: _selectedUnit,
|
||||
unitType: _selectedUnitType,
|
||||
frequencyPerDay: _frequencyPerDay,
|
||||
reminderTimes: _reminderTimes,
|
||||
@@ -380,9 +624,15 @@ class _AddSupplementScreenState extends State<AddSupplementScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_dosageAmountController.dispose();
|
||||
_brandController.dispose();
|
||||
_numberOfUnitsController.dispose();
|
||||
_notesController.dispose();
|
||||
|
||||
// Dispose all ingredient controllers
|
||||
for (final controller in _ingredientControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
385
lib/screens/archived_supplements_screen.dart
Normal file
385
lib/screens/archived_supplements_screen.dart
Normal file
@@ -0,0 +1,385 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/supplement_provider.dart';
|
||||
import '../models/supplement.dart';
|
||||
|
||||
class ArchivedSupplementsScreen extends StatefulWidget {
|
||||
const ArchivedSupplementsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ArchivedSupplementsScreen> createState() => _ArchivedSupplementsScreenState();
|
||||
}
|
||||
|
||||
class _ArchivedSupplementsScreenState extends State<ArchivedSupplementsScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<SupplementProvider>().loadArchivedSupplements();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Archived Supplements'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: Consumer<SupplementProvider>(
|
||||
builder: (context, provider, child) {
|
||||
if (provider.archivedSupplements.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.archive_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No archived supplements',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Archived supplements will appear here',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await provider.loadArchivedSupplements();
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: provider.archivedSupplements.length,
|
||||
itemBuilder: (context, index) {
|
||||
final supplement = provider.archivedSupplements[index];
|
||||
return _ArchivedSupplementCard(
|
||||
supplement: supplement,
|
||||
onUnarchive: () => _unarchiveSupplement(context, supplement),
|
||||
onDelete: () => _deleteSupplement(context, supplement),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _unarchiveSupplement(BuildContext context, Supplement supplement) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Unarchive Supplement'),
|
||||
content: Text('Are you sure you want to unarchive ${supplement.name}?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<SupplementProvider>().unarchiveSupplement(supplement.id!);
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${supplement.name} unarchived'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Unarchive'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _deleteSupplement(BuildContext context, Supplement supplement) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete Supplement'),
|
||||
content: Text(
|
||||
'Are you sure you want to permanently delete ${supplement.name}? This action cannot be undone.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<SupplementProvider>().deleteArchivedSupplement(supplement.id!);
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${supplement.name} deleted permanently'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: const Text('Delete', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArchivedSupplementCard extends StatelessWidget {
|
||||
final Supplement supplement;
|
||||
final VoidCallback onUnarchive;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const _ArchivedSupplementCard({
|
||||
required this.supplement,
|
||||
required this.onUnarchive,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.archive,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
supplement.name,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (supplement.brand != null && supplement.brand!.isNotEmpty)
|
||||
Text(
|
||||
supplement.brand!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'unarchive':
|
||||
onUnarchive();
|
||||
break;
|
||||
case 'delete':
|
||||
onDelete();
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'unarchive',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.unarchive, color: Colors.green),
|
||||
SizedBox(width: 8),
|
||||
Text('Unarchive'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete_forever, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('Delete Permanently', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Supplement details in a muted style
|
||||
if (supplement.ingredients.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Ingredients per ${supplement.unitType}:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: supplement.ingredients.map((ingredient) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'${ingredient.name} ${ingredient.amount}${ingredient.unit}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// Dosage info
|
||||
Row(
|
||||
children: [
|
||||
_InfoChip(
|
||||
icon: Icons.schedule,
|
||||
label: '${supplement.frequencyPerDay}x daily',
|
||||
context: context,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_InfoChip(
|
||||
icon: Icons.medication,
|
||||
label: '${supplement.numberOfUnits} ${supplement.unitType}',
|
||||
context: context,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (supplement.reminderTimes.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
_InfoChip(
|
||||
icon: Icons.notifications_off,
|
||||
label: 'Was: ${supplement.reminderTimes.join(', ')}',
|
||||
context: context,
|
||||
fullWidth: true,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final BuildContext context;
|
||||
final bool fullWidth;
|
||||
|
||||
const _InfoChip({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.context,
|
||||
this.fullWidth = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: fullWidth ? double.infinity : null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
int _selectedMonth = DateTime.now().month;
|
||||
int _selectedYear = DateTime.now().year;
|
||||
int _refreshKey = 0; // Add this to force FutureBuilder refresh
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -90,6 +91,7 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
||||
),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
key: ValueKey('daily_view_$_refreshKey'), // Use refresh key to force rebuild
|
||||
future: context.read<SupplementProvider>().getIntakesForDate(_selectedDate),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
@@ -137,12 +139,10 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
||||
final supplementIntakes = groupedIntakes[supplementName]!;
|
||||
|
||||
// Calculate totals
|
||||
double totalDosage = 0;
|
||||
double totalUnits = 0;
|
||||
final firstIntake = supplementIntakes.first;
|
||||
|
||||
for (final intake in supplementIntakes) {
|
||||
totalDosage += intake['dosageTaken'] as double;
|
||||
totalUnits += (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0;
|
||||
}
|
||||
|
||||
@@ -161,14 +161,14 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${totalDosage.toStringAsFixed(totalDosage % 1 == 0 ? 0 : 1)} ${firstIntake['supplementUnit']} total',
|
||||
'${totalUnits.toStringAsFixed(totalUnits % 1 == 0 ? 0 : 1)} ${firstIntake['supplementUnitType'] ?? 'units'} total',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${totalUnits.toStringAsFixed(totalUnits % 1 == 0 ? 0 : 1)} ${firstIntake['supplementUnitType'] ?? 'units'} • ${supplementIntakes.length} intake${supplementIntakes.length > 1 ? 's' : ''}',
|
||||
'${supplementIntakes.length} intake${supplementIntakes.length > 1 ? 's' : ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
@@ -181,9 +181,9 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
||||
final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0;
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 72, right: 16),
|
||||
contentPadding: const EdgeInsets.only(left: 72, right: 8),
|
||||
title: Text(
|
||||
'${(intake['dosageTaken'] as double).toStringAsFixed((intake['dosageTaken'] as double) % 1 == 0 ? 0 : 1)} ${intake['supplementUnit']}',
|
||||
'${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Column(
|
||||
@@ -210,6 +210,19 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red.shade400,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => _deleteIntake(context, intake['id'], intake['supplementName']),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
@@ -335,6 +348,7 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
||||
const SizedBox(height: 8),
|
||||
...dayIntakes.map((intake) {
|
||||
final takenAt = DateTime.parse(intake['takenAt']);
|
||||
final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
@@ -347,10 +361,23 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${intake['supplementName']} - ${intake['dosageTaken']} ${intake['supplementUnit']} at ${DateFormat('HH:mm').format(takenAt)}',
|
||||
'${intake['supplementName']} - ${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'} at ${DateFormat('HH:mm').format(takenAt)}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red.shade400,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () => _deleteIntake(context, intake['id'], intake['supplementName']),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 24,
|
||||
minHeight: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -403,16 +430,27 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<SupplementProvider>().deleteIntake(intakeId);
|
||||
onPressed: () async {
|
||||
await context.read<SupplementProvider>().deleteIntake(intakeId);
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Force refresh of the UI
|
||||
setState(() {
|
||||
_refreshKey++; // This will force FutureBuilder to rebuild
|
||||
});
|
||||
|
||||
// Force refresh of the current view data
|
||||
if (_tabController.index == 1) {
|
||||
// Monthly view - refresh monthly intakes
|
||||
context.read<SupplementProvider>().loadMonthlyIntakes(_selectedYear, _selectedMonth);
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Intake deleted'),
|
||||
SnackBar(
|
||||
content: Text('$supplementName intake deleted'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
setState(() {}); // Refresh the view
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: const Text('Delete'),
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/supplement_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import 'supplements_list_screen.dart';
|
||||
import 'history_screen.dart';
|
||||
import 'add_supplement_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@@ -18,6 +20,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
final List<Widget> _screens = [
|
||||
const SupplementsListScreen(),
|
||||
const HistoryScreen(),
|
||||
const SettingsScreen(),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -25,9 +28,42 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<SupplementProvider>().initialize();
|
||||
_startPersistentReminderCheck();
|
||||
});
|
||||
}
|
||||
|
||||
void _startPersistentReminderCheck() {
|
||||
// Check immediately and then every 10 minutes
|
||||
_checkPersistentReminders();
|
||||
|
||||
// Set up periodic checking
|
||||
Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(minutes: 10));
|
||||
if (mounted) {
|
||||
await _checkPersistentReminders();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkPersistentReminders() async {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final supplementProvider = context.read<SupplementProvider>();
|
||||
final settingsProvider = context.read<SettingsProvider>();
|
||||
|
||||
await supplementProvider.checkPersistentRemindersWithSettings(
|
||||
persistentReminders: settingsProvider.persistentReminders,
|
||||
reminderRetryInterval: settingsProvider.reminderRetryInterval,
|
||||
maxRetryAttempts: settingsProvider.maxRetryAttempts,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error checking persistent reminders: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -39,6 +75,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
type: BottomNavigationBarType.fixed,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.medication),
|
||||
@@ -48,6 +85,10 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
icon: Icon(Icons.history),
|
||||
label: 'History',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: _currentIndex == 0
|
||||
|
601
lib/screens/settings_screen.dart
Normal file
601
lib/screens/settings_screen.dart
Normal file
@@ -0,0 +1,601 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../providers/supplement_provider.dart';
|
||||
import '../services/notification_service.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
),
|
||||
body: Consumer<SettingsProvider>(
|
||||
builder: (context, settingsProvider, child) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Theme',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
RadioListTile<ThemeOption>(
|
||||
title: const Text('Follow System'),
|
||||
subtitle: const Text('Use system theme setting'),
|
||||
value: ThemeOption.system,
|
||||
groupValue: settingsProvider.themeOption,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsProvider.setThemeOption(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<ThemeOption>(
|
||||
title: const Text('Light Theme'),
|
||||
subtitle: const Text('Always use light theme'),
|
||||
value: ThemeOption.light,
|
||||
groupValue: settingsProvider.themeOption,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsProvider.setThemeOption(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<ThemeOption>(
|
||||
title: const Text('Dark Theme'),
|
||||
subtitle: const Text('Always use dark theme'),
|
||||
value: ThemeOption.dark,
|
||||
groupValue: settingsProvider.themeOption,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsProvider.setThemeOption(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Time Periods',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Customize when morning, afternoon, evening, and night periods occur',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_TimeRangeSelector(
|
||||
title: 'Morning',
|
||||
icon: Icons.wb_sunny,
|
||||
color: Colors.orange,
|
||||
startHour: settingsProvider.morningStart,
|
||||
endHour: settingsProvider.morningEnd,
|
||||
onChanged: (start, end) => _updateTimeRanges(
|
||||
context, settingsProvider,
|
||||
morningStart: start, morningEnd: end,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_TimeRangeSelector(
|
||||
title: 'Afternoon',
|
||||
icon: Icons.light_mode,
|
||||
color: Colors.blue,
|
||||
startHour: settingsProvider.afternoonStart,
|
||||
endHour: settingsProvider.afternoonEnd,
|
||||
onChanged: (start, end) => _updateTimeRanges(
|
||||
context, settingsProvider,
|
||||
afternoonStart: start, afternoonEnd: end,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_TimeRangeSelector(
|
||||
title: 'Evening',
|
||||
icon: Icons.nightlight_round,
|
||||
color: Colors.indigo,
|
||||
startHour: settingsProvider.eveningStart,
|
||||
endHour: settingsProvider.eveningEnd,
|
||||
onChanged: (start, end) => _updateTimeRanges(
|
||||
context, settingsProvider,
|
||||
eveningStart: start, eveningEnd: end,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_TimeRangeSelector(
|
||||
title: 'Night',
|
||||
icon: Icons.bedtime,
|
||||
color: Colors.purple,
|
||||
startHour: settingsProvider.nightStart,
|
||||
endHour: settingsProvider.nightEnd,
|
||||
onChanged: (start, end) => _updateTimeRanges(
|
||||
context, settingsProvider,
|
||||
nightStart: start, nightEnd: end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.notifications_active, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Persistent Reminders',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Configure automatic reminder retries for ignored notifications',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Enable Persistent Reminders'),
|
||||
subtitle: const Text('Resend notifications if ignored'),
|
||||
value: settingsProvider.persistentReminders,
|
||||
onChanged: (value) {
|
||||
settingsProvider.setPersistentReminders(value);
|
||||
},
|
||||
),
|
||||
if (settingsProvider.persistentReminders) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Retry Interval',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SegmentedButton<int>(
|
||||
segments: const [
|
||||
ButtonSegment(value: 5, label: Text('5 min')),
|
||||
ButtonSegment(value: 10, label: Text('10 min')),
|
||||
ButtonSegment(value: 15, label: Text('15 min')),
|
||||
ButtonSegment(value: 30, label: Text('30 min')),
|
||||
],
|
||||
selected: {settingsProvider.reminderRetryInterval},
|
||||
onSelectionChanged: (values) {
|
||||
settingsProvider.setReminderRetryInterval(values.first);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Maximum Retry Attempts',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SegmentedButton<int>(
|
||||
segments: const [
|
||||
ButtonSegment(value: 1, label: Text('1')),
|
||||
ButtonSegment(value: 2, label: Text('2')),
|
||||
ButtonSegment(value: 3, label: Text('3')),
|
||||
ButtonSegment(value: 5, label: Text('5')),
|
||||
],
|
||||
selected: {settingsProvider.maxRetryAttempts},
|
||||
onSelectionChanged: (values) {
|
||||
settingsProvider.setMaxRetryAttempts(values.first);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (Theme.of(context).brightness == Brightness.dark) // Only show in debug mode for now
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.bug_report, color: Colors.orange),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Debug - Notifications',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Consumer<SupplementProvider>(
|
||||
builder: (context, supplementProvider, child) {
|
||||
return Column(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
await supplementProvider.testNotifications();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Test notification sent!')),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.notifications_active),
|
||||
label: const Text('Test Instant'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
await supplementProvider.testScheduledNotification();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Scheduled test notification for 1 minute from now!')),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.schedule),
|
||||
label: const Text('Test Scheduled (1min)'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
await supplementProvider.testNotificationActions();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Test notification with actions sent! Try the Take/Snooze buttons.')),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.touch_app),
|
||||
label: const Text('Test Actions'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
await NotificationService().testBasicNotification();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Basic test notification sent! Tap it to test callback.')),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.tap_and_play),
|
||||
label: const Text('Test Basic Tap'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
await supplementProvider.rescheduleAllNotifications();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('All notifications rescheduled!')),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Reschedule All'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
await supplementProvider.cancelAllNotifications();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('All notifications cancelled!')),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: const Text('Cancel All'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final pending = await supplementProvider.getPendingNotifications();
|
||||
if (context.mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Pending Notifications'),
|
||||
content: pending.isEmpty
|
||||
? const Text('No pending notifications')
|
||||
: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: Consumer<SupplementProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: pending.length,
|
||||
itemBuilder: (context, index) {
|
||||
final notification = pending[index];
|
||||
|
||||
// Calculate scheduled time inline
|
||||
String scheduledTime = '';
|
||||
try {
|
||||
final notificationId = notification.id;
|
||||
if (notificationId == 99999) {
|
||||
scheduledTime = 'Test notification';
|
||||
} else if (notificationId > 1000) {
|
||||
final snoozeMinutes = notificationId % 1000;
|
||||
scheduledTime = 'Snoozed ($snoozeMinutes min)';
|
||||
} else {
|
||||
final supplementId = notificationId ~/ 100;
|
||||
final reminderIndex = notificationId % 100;
|
||||
|
||||
final supplement = provider.supplements.firstWhere(
|
||||
(s) => s.id == supplementId,
|
||||
orElse: () => provider.supplements.first,
|
||||
);
|
||||
|
||||
if (reminderIndex < supplement.reminderTimes.length) {
|
||||
final reminderTime = supplement.reminderTimes[reminderIndex];
|
||||
final now = DateTime.now();
|
||||
final timeParts = reminderTime.split(':');
|
||||
final hour = int.parse(timeParts[0]);
|
||||
final minute = int.parse(timeParts[1]);
|
||||
|
||||
final today = DateTime(now.year, now.month, now.day, hour, minute);
|
||||
final isToday = today.isAfter(now);
|
||||
|
||||
scheduledTime = '${isToday ? 'Today' : 'Tomorrow'} at $reminderTime';
|
||||
} else {
|
||||
scheduledTime = 'Unknown time';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
scheduledTime = 'ID: ${notification.id}';
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
notification.title ?? 'No title',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('ID: ${notification.id}'),
|
||||
Text(notification.body ?? 'No body'),
|
||||
if (scheduledTime.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'⏰ $scheduledTime',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.list),
|
||||
label: const Text('Show Pending'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateTimeRanges(
|
||||
BuildContext context,
|
||||
SettingsProvider settingsProvider, {
|
||||
int? morningStart,
|
||||
int? morningEnd,
|
||||
int? afternoonStart,
|
||||
int? afternoonEnd,
|
||||
int? eveningStart,
|
||||
int? eveningEnd,
|
||||
int? nightStart,
|
||||
int? nightEnd,
|
||||
}) async {
|
||||
try {
|
||||
await settingsProvider.setTimeRanges(
|
||||
morningStart: morningStart ?? settingsProvider.morningStart,
|
||||
morningEnd: morningEnd ?? settingsProvider.morningEnd,
|
||||
afternoonStart: afternoonStart ?? settingsProvider.afternoonStart,
|
||||
afternoonEnd: afternoonEnd ?? settingsProvider.afternoonEnd,
|
||||
eveningStart: eveningStart ?? settingsProvider.eveningStart,
|
||||
eveningEnd: eveningEnd ?? settingsProvider.eveningEnd,
|
||||
nightStart: nightStart ?? settingsProvider.nightStart,
|
||||
nightEnd: nightEnd ?? settingsProvider.nightEnd,
|
||||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Invalid time ranges: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _TimeRangeSelector extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final int startHour;
|
||||
final int endHour;
|
||||
final void Function(int start, int end) onChanged;
|
||||
|
||||
const _TimeRangeSelector({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.startHour,
|
||||
required this.endHour,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${_formatHour(startHour)} - ${_formatHour(endHour + 1)}',
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Start: ${_formatHour(startHour)}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
Slider(
|
||||
value: startHour.toDouble(),
|
||||
min: 0,
|
||||
max: 23,
|
||||
divisions: 23,
|
||||
activeColor: color,
|
||||
onChanged: (value) {
|
||||
final newStart = value.round();
|
||||
if (newStart != endHour) {
|
||||
onChanged(newStart, endHour);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'End: ${_formatHour(endHour)}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
Slider(
|
||||
value: endHour.toDouble(),
|
||||
min: 0,
|
||||
max: 23,
|
||||
divisions: 23,
|
||||
activeColor: color,
|
||||
onChanged: (value) {
|
||||
final newEnd = value.round();
|
||||
if (newEnd != startHour) {
|
||||
onChanged(startHour, newEnd);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatHour(int hour) {
|
||||
final adjustedHour = hour % 24;
|
||||
return '${adjustedHour.toString().padLeft(2, '0')}:00';
|
||||
}
|
||||
}
|
@@ -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