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:
259
lib/providers/settings_provider.dart
Normal file
259
lib/providers/settings_provider.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
enum ThemeOption {
|
||||
system,
|
||||
light,
|
||||
dark,
|
||||
}
|
||||
|
||||
class SettingsProvider extends ChangeNotifier {
|
||||
ThemeOption _themeOption = ThemeOption.system;
|
||||
|
||||
// Time range settings (stored as hours, 0-23)
|
||||
int _morningStart = 5;
|
||||
int _morningEnd = 10;
|
||||
int _afternoonStart = 11;
|
||||
int _afternoonEnd = 16;
|
||||
int _eveningStart = 17;
|
||||
int _eveningEnd = 22;
|
||||
int _nightStart = 23;
|
||||
int _nightEnd = 4;
|
||||
|
||||
// Persistent reminder settings
|
||||
bool _persistentReminders = true;
|
||||
int _reminderRetryInterval = 5; // minutes
|
||||
int _maxRetryAttempts = 3;
|
||||
|
||||
ThemeOption get themeOption => _themeOption;
|
||||
|
||||
// Time range getters
|
||||
int get morningStart => _morningStart;
|
||||
int get morningEnd => _morningEnd;
|
||||
int get afternoonStart => _afternoonStart;
|
||||
int get afternoonEnd => _afternoonEnd;
|
||||
int get eveningStart => _eveningStart;
|
||||
int get eveningEnd => _eveningEnd;
|
||||
int get nightStart => _nightStart;
|
||||
int get nightEnd => _nightEnd;
|
||||
|
||||
// Persistent reminder getters
|
||||
bool get persistentReminders => _persistentReminders;
|
||||
int get reminderRetryInterval => _reminderRetryInterval;
|
||||
int get maxRetryAttempts => _maxRetryAttempts;
|
||||
|
||||
// Helper method to get formatted time ranges for display
|
||||
String get morningRange => '${_formatHour(_morningStart)} - ${_formatHour((_morningEnd + 1) % 24)}';
|
||||
String get afternoonRange => '${_formatHour(_afternoonStart)} - ${_formatHour((_afternoonEnd + 1) % 24)}';
|
||||
String get eveningRange => '${_formatHour(_eveningStart)} - ${_formatHour((_eveningEnd + 1) % 24)}';
|
||||
String get nightRange => '${_formatHour(_nightStart)} - ${_formatHour((_nightEnd + 1) % 24)}';
|
||||
|
||||
String _formatHour(int hour) {
|
||||
return '${hour.toString().padLeft(2, '0')}:00';
|
||||
}
|
||||
|
||||
ThemeMode get themeMode {
|
||||
switch (_themeOption) {
|
||||
case ThemeOption.light:
|
||||
return ThemeMode.light;
|
||||
case ThemeOption.dark:
|
||||
return ThemeMode.dark;
|
||||
case ThemeOption.system:
|
||||
return ThemeMode.system;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final themeIndex = prefs.getInt('theme_option') ?? 0;
|
||||
_themeOption = ThemeOption.values[themeIndex];
|
||||
|
||||
// Load time range settings
|
||||
_morningStart = prefs.getInt('morning_start') ?? 5;
|
||||
_morningEnd = prefs.getInt('morning_end') ?? 10;
|
||||
_afternoonStart = prefs.getInt('afternoon_start') ?? 11;
|
||||
_afternoonEnd = prefs.getInt('afternoon_end') ?? 16;
|
||||
_eveningStart = prefs.getInt('evening_start') ?? 17;
|
||||
_eveningEnd = prefs.getInt('evening_end') ?? 22;
|
||||
_nightStart = prefs.getInt('night_start') ?? 23;
|
||||
_nightEnd = prefs.getInt('night_end') ?? 4;
|
||||
|
||||
// Load persistent reminder settings
|
||||
_persistentReminders = prefs.getBool('persistent_reminders') ?? true;
|
||||
_reminderRetryInterval = prefs.getInt('reminder_retry_interval') ?? 5;
|
||||
_maxRetryAttempts = prefs.getInt('max_retry_attempts') ?? 3;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setThemeOption(ThemeOption option) async {
|
||||
_themeOption = option;
|
||||
notifyListeners();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('theme_option', option.index);
|
||||
}
|
||||
|
||||
Future<void> setTimeRanges({
|
||||
required int morningStart,
|
||||
required int morningEnd,
|
||||
required int afternoonStart,
|
||||
required int afternoonEnd,
|
||||
required int eveningStart,
|
||||
required int eveningEnd,
|
||||
required int nightStart,
|
||||
required int nightEnd,
|
||||
}) async {
|
||||
// Validate ranges don't overlap (simplified validation)
|
||||
if (!_areTimeRangesValid(
|
||||
morningStart, morningEnd,
|
||||
afternoonStart, afternoonEnd,
|
||||
eveningStart, eveningEnd,
|
||||
nightStart, nightEnd,
|
||||
)) {
|
||||
throw ArgumentError('Time ranges overlap or are invalid');
|
||||
}
|
||||
|
||||
_morningStart = morningStart;
|
||||
_morningEnd = morningEnd;
|
||||
_afternoonStart = afternoonStart;
|
||||
_afternoonEnd = afternoonEnd;
|
||||
_eveningStart = eveningStart;
|
||||
_eveningEnd = eveningEnd;
|
||||
_nightStart = nightStart;
|
||||
_nightEnd = nightEnd;
|
||||
|
||||
notifyListeners();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('morning_start', morningStart);
|
||||
await prefs.setInt('morning_end', morningEnd);
|
||||
await prefs.setInt('afternoon_start', afternoonStart);
|
||||
await prefs.setInt('afternoon_end', afternoonEnd);
|
||||
await prefs.setInt('evening_start', eveningStart);
|
||||
await prefs.setInt('evening_end', eveningEnd);
|
||||
await prefs.setInt('night_start', nightStart);
|
||||
await prefs.setInt('night_end', nightEnd);
|
||||
}
|
||||
|
||||
bool _areTimeRangesValid(
|
||||
int morningStart, int morningEnd,
|
||||
int afternoonStart, int afternoonEnd,
|
||||
int eveningStart, int eveningEnd,
|
||||
int nightStart, int nightEnd,
|
||||
) {
|
||||
// Basic validation - ensure start < end for non-wrapping periods
|
||||
if (morningStart > morningEnd) return false;
|
||||
if (afternoonStart > afternoonEnd) return false;
|
||||
if (eveningStart > eveningEnd) return false;
|
||||
|
||||
// Night can wrap around midnight, so we allow nightStart > nightEnd
|
||||
|
||||
// Check for overlaps in sequential periods
|
||||
if (morningEnd >= afternoonStart) return false;
|
||||
if (afternoonEnd >= eveningStart) return false;
|
||||
if (eveningEnd >= nightStart) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method to determine time category based on current settings
|
||||
String determineTimeCategory(List<String> reminderTimes) {
|
||||
if (reminderTimes.isEmpty) {
|
||||
return 'anytime';
|
||||
}
|
||||
|
||||
// Convert reminder times to hours for categorization
|
||||
final hours = reminderTimes.map((time) {
|
||||
final parts = time.split(':');
|
||||
return int.tryParse(parts[0]) ?? 12;
|
||||
}).toList();
|
||||
|
||||
// Count how many times fall into each category
|
||||
int morningCount = 0;
|
||||
int afternoonCount = 0;
|
||||
int eveningCount = 0;
|
||||
int nightCount = 0;
|
||||
|
||||
for (final hour in hours) {
|
||||
if (hour >= _morningStart && hour <= _morningEnd) {
|
||||
morningCount++;
|
||||
} else if (hour >= _afternoonStart && hour <= _afternoonEnd) {
|
||||
afternoonCount++;
|
||||
} else if (hour >= _eveningStart && hour <= _eveningEnd) {
|
||||
eveningCount++;
|
||||
} else if (_isInNightRange(hour)) {
|
||||
nightCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If supplement is taken throughout the day (has times in multiple periods)
|
||||
final periodsCount = (morningCount > 0 ? 1 : 0) +
|
||||
(afternoonCount > 0 ? 1 : 0) +
|
||||
(eveningCount > 0 ? 1 : 0) +
|
||||
(nightCount > 0 ? 1 : 0);
|
||||
|
||||
if (periodsCount >= 2) {
|
||||
// Categorize based on the earliest reminder time for consistency
|
||||
final earliestHour = hours.reduce((a, b) => a < b ? a : b);
|
||||
if (earliestHour >= _morningStart && earliestHour <= _morningEnd) {
|
||||
return 'morning';
|
||||
} else if (earliestHour >= _afternoonStart && earliestHour <= _afternoonEnd) {
|
||||
return 'afternoon';
|
||||
} else if (earliestHour >= _eveningStart && earliestHour <= _eveningEnd) {
|
||||
return 'evening';
|
||||
} else if (_isInNightRange(earliestHour)) {
|
||||
return 'night';
|
||||
}
|
||||
}
|
||||
|
||||
// If all times are in one period, categorize accordingly
|
||||
if (morningCount > 0) {
|
||||
return 'morning';
|
||||
} else if (afternoonCount > 0) {
|
||||
return 'afternoon';
|
||||
} else if (eveningCount > 0) {
|
||||
return 'evening';
|
||||
} else if (nightCount > 0) {
|
||||
return 'night';
|
||||
} else {
|
||||
return 'anytime';
|
||||
}
|
||||
}
|
||||
|
||||
bool _isInNightRange(int hour) {
|
||||
// Night range can wrap around midnight
|
||||
if (_nightStart <= _nightEnd) {
|
||||
// Normal range (doesn't wrap around midnight)
|
||||
return hour >= _nightStart && hour <= _nightEnd;
|
||||
} else {
|
||||
// Wrapping range (e.g., 23:00 to 4:00)
|
||||
return hour >= _nightStart || hour <= _nightEnd;
|
||||
}
|
||||
}
|
||||
|
||||
// Persistent reminder setters
|
||||
Future<void> setPersistentReminders(bool enabled) async {
|
||||
_persistentReminders = enabled;
|
||||
notifyListeners();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('persistent_reminders', enabled);
|
||||
}
|
||||
|
||||
Future<void> setReminderRetryInterval(int minutes) async {
|
||||
_reminderRetryInterval = minutes;
|
||||
notifyListeners();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('reminder_retry_interval', minutes);
|
||||
}
|
||||
|
||||
Future<void> setMaxRetryAttempts(int attempts) async {
|
||||
_maxRetryAttempts = attempts;
|
||||
notifyListeners();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('max_retry_attempts', attempts);
|
||||
}
|
||||
}
|
@@ -1,4 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import '../models/supplement.dart';
|
||||
import '../models/supplement_intake.dart';
|
||||
import '../services/database_helper.dart';
|
||||
@@ -12,6 +14,7 @@ class SupplementProvider with ChangeNotifier {
|
||||
List<Map<String, dynamic>> _todayIntakes = [];
|
||||
List<Map<String, dynamic>> _monthlyIntakes = [];
|
||||
bool _isLoading = false;
|
||||
Timer? _persistentReminderTimer;
|
||||
|
||||
List<Supplement> get supplements => _supplements;
|
||||
List<Map<String, dynamic>> get todayIntakes => _todayIntakes;
|
||||
@@ -20,9 +23,115 @@ class SupplementProvider with ChangeNotifier {
|
||||
|
||||
Future<void> initialize() async {
|
||||
await _notificationService.initialize();
|
||||
await _notificationService.requestPermissions();
|
||||
|
||||
// Set up the callback for handling supplement intake from notifications
|
||||
print('📱 Setting up notification callback...');
|
||||
_notificationService.setTakeSupplementCallback((supplementId, supplementName, units, unitType) {
|
||||
print('📱 === NOTIFICATION CALLBACK TRIGGERED ===');
|
||||
print('📱 Supplement ID: $supplementId');
|
||||
print('📱 Supplement Name: $supplementName');
|
||||
print('📱 Units: $units');
|
||||
print('📱 Unit Type: $unitType');
|
||||
|
||||
// Record the intake when user taps "Take" on notification
|
||||
recordIntake(supplementId, 0.0, unitsTaken: units);
|
||||
print('📱 Intake recorded successfully');
|
||||
print('📱 === CALLBACK COMPLETE ===');
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📱 Recorded intake from notification: $supplementName ($units $unitType)');
|
||||
}
|
||||
});
|
||||
print('📱 Notification callback setup complete');
|
||||
|
||||
// Request permissions with error handling
|
||||
try {
|
||||
await _notificationService.requestPermissions();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error requesting notification permissions: $e');
|
||||
}
|
||||
// Continue without notifications rather than crashing
|
||||
}
|
||||
|
||||
await loadSupplements();
|
||||
await loadTodayIntakes();
|
||||
|
||||
// Reschedule notifications for all active supplements to ensure persistence
|
||||
await _rescheduleAllNotifications();
|
||||
|
||||
// Start periodic checking for persistent reminders (every 5 minutes)
|
||||
_startPersistentReminderCheck();
|
||||
}
|
||||
|
||||
void _startPersistentReminderCheck() {
|
||||
// Cancel any existing timer
|
||||
_persistentReminderTimer?.cancel();
|
||||
|
||||
// Check every 5 minutes for persistent reminders
|
||||
_persistentReminderTimer = Timer.periodic(const Duration(minutes: 5), (timer) async {
|
||||
try {
|
||||
// This will be called from settings provider context, so we need to import it
|
||||
await _checkPersistentReminders();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error checking persistent reminders: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Also check immediately
|
||||
_checkPersistentReminders();
|
||||
}
|
||||
|
||||
Future<void> _checkPersistentReminders() async {
|
||||
// This method will be enhanced to accept settings from the UI layer
|
||||
// For now, we'll check with default settings
|
||||
// In practice, the UI should call checkPersistentRemindersWithSettings
|
||||
if (kDebugMode) {
|
||||
print('📱 Checking persistent reminders with default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Method to be called from UI with actual settings
|
||||
Future<void> checkPersistentRemindersWithSettings({
|
||||
required bool persistentReminders,
|
||||
required int reminderRetryInterval,
|
||||
required int maxRetryAttempts,
|
||||
}) async {
|
||||
await _notificationService.checkPersistentReminders(
|
||||
persistentReminders,
|
||||
reminderRetryInterval,
|
||||
maxRetryAttempts,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_persistentReminderTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _rescheduleAllNotifications() async {
|
||||
if (kDebugMode) {
|
||||
print('📱 Rescheduling notifications for all active supplements...');
|
||||
}
|
||||
|
||||
for (final supplement in _supplements) {
|
||||
if (supplement.reminderTimes.isNotEmpty) {
|
||||
try {
|
||||
await _notificationService.scheduleSupplementReminders(supplement);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('📱 Error rescheduling notifications for ${supplement.name}: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📱 Finished rescheduling notifications');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadSupplements() async {
|
||||
@@ -103,11 +212,11 @@ class SupplementProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recordIntake(int supplementId, double dosage, {double? unitsTaken, String? notes}) async {
|
||||
Future<void> recordIntake(int supplementId, double dosage, {double? unitsTaken, String? notes, DateTime? takenAt}) async {
|
||||
try {
|
||||
final intake = SupplementIntake(
|
||||
supplementId: supplementId,
|
||||
takenAt: DateTime.now(),
|
||||
takenAt: takenAt ?? DateTime.now(),
|
||||
dosageTaken: dosage,
|
||||
unitsTaken: unitsTaken ?? 1.0,
|
||||
notes: notes,
|
||||
@@ -121,7 +230,7 @@ class SupplementProvider with ChangeNotifier {
|
||||
final unitsText = unitsTaken != null && unitsTaken != 1 ? '${unitsTaken.toStringAsFixed(unitsTaken % 1 == 0 ? 0 : 1)} ${supplement.unitType}' : '';
|
||||
await _notificationService.showInstantNotification(
|
||||
'Supplement Taken',
|
||||
'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} ($dosage ${supplement.unit})',
|
||||
'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} (${supplement.ingredientsDisplay})',
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
@@ -167,10 +276,100 @@ class SupplementProvider with ChangeNotifier {
|
||||
try {
|
||||
await _databaseHelper.deleteIntake(intakeId);
|
||||
await loadTodayIntakes();
|
||||
// Also refresh monthly intakes if they're loaded
|
||||
if (_monthlyIntakes.isNotEmpty) {
|
||||
await loadMonthlyIntakes(DateTime.now().year, DateTime.now().month);
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error deleting intake: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool hasBeenTakenToday(int supplementId) {
|
||||
return _todayIntakes.any((intake) => intake['supplement_id'] == supplementId);
|
||||
}
|
||||
|
||||
int getTodayIntakeCount(int supplementId) {
|
||||
return _todayIntakes.where((intake) => intake['supplement_id'] == supplementId).length;
|
||||
}
|
||||
|
||||
// Archive functionality
|
||||
List<Supplement> _archivedSupplements = [];
|
||||
List<Supplement> get archivedSupplements => _archivedSupplements;
|
||||
|
||||
Future<void> loadArchivedSupplements() async {
|
||||
try {
|
||||
_archivedSupplements = await _databaseHelper.getArchivedSupplements();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error loading archived supplements: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> archiveSupplement(int supplementId) async {
|
||||
try {
|
||||
await _databaseHelper.archiveSupplement(supplementId);
|
||||
await loadSupplements(); // Refresh active supplements
|
||||
await loadArchivedSupplements(); // Refresh archived supplements
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error archiving supplement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unarchiveSupplement(int supplementId) async {
|
||||
try {
|
||||
await _databaseHelper.unarchiveSupplement(supplementId);
|
||||
await loadSupplements(); // Refresh active supplements
|
||||
await loadArchivedSupplements(); // Refresh archived supplements
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error unarchiving supplement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteArchivedSupplement(int supplementId) async {
|
||||
try {
|
||||
await _databaseHelper.deleteSupplement(supplementId);
|
||||
await loadArchivedSupplements(); // Refresh archived supplements
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error deleting archived supplement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug methods for notification testing
|
||||
Future<void> testNotifications() async {
|
||||
await _notificationService.testNotification();
|
||||
}
|
||||
|
||||
Future<void> testScheduledNotification() async {
|
||||
await _notificationService.testScheduledNotification();
|
||||
}
|
||||
|
||||
Future<void> testNotificationActions() async {
|
||||
await _notificationService.testNotificationWithActions();
|
||||
}
|
||||
|
||||
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
|
||||
return await _notificationService.getPendingNotifications();
|
||||
}
|
||||
|
||||
// Debug method to test notification persistence
|
||||
Future<void> rescheduleAllNotifications() async {
|
||||
await _rescheduleAllNotifications();
|
||||
}
|
||||
|
||||
// Debug method to cancel all notifications
|
||||
Future<void> cancelAllNotifications() async {
|
||||
await _notificationService.cancelAllReminders();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user