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:
2025-08-26 17:19:54 +02:00
parent e6181add08
commit 2aec59ec35
18 changed files with 3756 additions and 376 deletions

View File

@@ -2,15 +2,17 @@ import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path/path.dart';
import 'dart:io';
import 'dart:convert';
import '../models/supplement.dart';
import '../models/supplement_intake.dart';
class DatabaseHelper {
static const _databaseName = 'supplements.db';
static const _databaseVersion = 2; // Increment version for schema changes
static const _databaseVersion = 5; // Increment version for notification tracking
static const supplementsTable = 'supplements';
static const intakesTable = 'supplement_intakes';
static const notificationTrackingTable = 'notification_tracking';
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
@@ -50,9 +52,9 @@ class DatabaseHelper {
CREATE TABLE $supplementsTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
dosageAmount REAL NOT NULL,
brand TEXT,
ingredients TEXT NOT NULL DEFAULT '[]',
numberOfUnits INTEGER NOT NULL DEFAULT 1,
unit TEXT NOT NULL,
unitType TEXT NOT NULL DEFAULT 'units',
frequencyPerDay INTEGER NOT NULL,
reminderTimes TEXT NOT NULL,
@@ -73,6 +75,20 @@ class DatabaseHelper {
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
)
''');
await db.execute('''
CREATE TABLE $notificationTrackingTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notificationId INTEGER NOT NULL UNIQUE,
supplementId INTEGER NOT NULL,
scheduledTime TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
retryCount INTEGER NOT NULL DEFAULT 0,
lastRetryTime TEXT,
createdAt TEXT NOT NULL,
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
)
''');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
@@ -97,6 +113,7 @@ class DatabaseHelper {
CREATE TABLE ${supplementsTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
brand TEXT,
dosageAmount REAL NOT NULL,
numberOfUnits INTEGER NOT NULL DEFAULT 1,
unit TEXT NOT NULL,
@@ -112,8 +129,8 @@ class DatabaseHelper {
// Copy data to new table
await db.execute('''
INSERT INTO ${supplementsTable}_new
(id, name, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive)
SELECT id, name, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive
(id, name, brand, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive)
SELECT id, name, NULL as brand, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive
FROM $supplementsTable
''');
@@ -121,6 +138,86 @@ class DatabaseHelper {
await db.execute('DROP TABLE $supplementsTable');
await db.execute('ALTER TABLE ${supplementsTable}_new RENAME TO $supplementsTable');
}
if (oldVersion < 3) {
// Add brand column for version 3
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN brand TEXT');
}
if (oldVersion < 4) {
// Complete migration to new ingredient-based schema
// Add ingredients column and migrate old data
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN ingredients TEXT DEFAULT "[]"');
// Migrate existing supplements to use ingredients format
final supplements = await db.query(supplementsTable);
for (final supplement in supplements) {
final dosageAmount = supplement['dosageAmount'] as double?;
final unit = supplement['unit'] as String?;
final name = supplement['name'] as String;
if (dosageAmount != null && unit != null && dosageAmount > 0) {
// Create a single ingredient from the old dosage data
final ingredient = {
'name': name,
'amount': dosageAmount,
'unit': unit,
};
final ingredientsJson = jsonEncode([ingredient]);
await db.update(
supplementsTable,
{'ingredients': ingredientsJson},
where: 'id = ?',
whereArgs: [supplement['id']],
);
}
}
// Remove old columns
await db.execute('''
CREATE TABLE ${supplementsTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
brand TEXT,
ingredients TEXT NOT NULL DEFAULT '[]',
numberOfUnits INTEGER NOT NULL DEFAULT 1,
unitType TEXT NOT NULL DEFAULT 'units',
frequencyPerDay INTEGER NOT NULL,
reminderTimes TEXT NOT NULL,
notes TEXT,
createdAt TEXT NOT NULL,
isActive INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
INSERT INTO ${supplementsTable}_new
(id, name, brand, ingredients, numberOfUnits, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive)
SELECT id, name, brand, ingredients, numberOfUnits, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive
FROM $supplementsTable
''');
await db.execute('DROP TABLE $supplementsTable');
await db.execute('ALTER TABLE ${supplementsTable}_new RENAME TO $supplementsTable');
}
if (oldVersion < 5) {
// Add notification tracking table
await db.execute('''
CREATE TABLE $notificationTrackingTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notificationId INTEGER NOT NULL UNIQUE,
supplementId INTEGER NOT NULL,
scheduledTime TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
retryCount INTEGER NOT NULL DEFAULT 0,
lastRetryTime TEXT,
createdAt TEXT NOT NULL,
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
)
''');
}
}
// Supplement CRUD operations
@@ -140,6 +237,37 @@ class DatabaseHelper {
return List.generate(maps.length, (i) => Supplement.fromMap(maps[i]));
}
Future<List<Supplement>> getArchivedSupplements() async {
Database db = await database;
List<Map<String, dynamic>> maps = await db.query(
supplementsTable,
where: 'isActive = ?',
whereArgs: [0],
orderBy: 'name ASC',
);
return List.generate(maps.length, (i) => Supplement.fromMap(maps[i]));
}
Future<void> archiveSupplement(int id) async {
Database db = await database;
await db.update(
supplementsTable,
{'isActive': 0},
where: 'id = ?',
whereArgs: [id],
);
}
Future<void> unarchiveSupplement(int id) async {
Database db = await database;
await db.update(
supplementsTable,
{'isActive': 1},
where: 'id = ?',
whereArgs: [id],
);
}
Future<Supplement?> getSupplement(int id) async {
Database db = await database;
List<Map<String, dynamic>> maps = await db.query(
@@ -213,7 +341,10 @@ class DatabaseHelper {
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*, s.name as supplementName, s.unit as supplementUnit, s.unitType as supplementUnitType
SELECT i.*,
i.supplementId as supplement_id,
s.name as supplementName,
s.unitType as supplementUnitType
FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ?
@@ -229,7 +360,10 @@ class DatabaseHelper {
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*, s.name as supplementName, s.unit as supplementUnit, s.unitType as supplementUnitType
SELECT i.*,
i.supplementId as supplement_id,
s.name as supplementName,
s.unitType as supplementUnitType
FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ?
@@ -247,4 +381,90 @@ class DatabaseHelper {
whereArgs: [id],
);
}
// Notification tracking methods
Future<int> trackNotification({
required int notificationId,
required int supplementId,
required DateTime scheduledTime,
}) async {
Database db = await database;
// Use INSERT OR REPLACE to handle both new and existing notifications
await db.rawInsert('''
INSERT OR REPLACE INTO $notificationTrackingTable
(notificationId, supplementId, scheduledTime, status, retryCount, lastRetryTime, createdAt)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', [
notificationId,
supplementId,
scheduledTime.toIso8601String(),
'pending',
0,
null,
DateTime.now().toIso8601String(),
]);
return notificationId;
}
Future<void> markNotificationTaken(int notificationId) async {
Database db = await database;
await db.update(
notificationTrackingTable,
{'status': 'taken'},
where: 'notificationId = ?',
whereArgs: [notificationId],
);
}
Future<void> incrementRetryCount(int notificationId) async {
Database db = await database;
await db.rawUpdate('''
UPDATE $notificationTrackingTable
SET retryCount = retryCount + 1,
lastRetryTime = ?,
status = 'retrying'
WHERE notificationId = ?
''', [DateTime.now().toIso8601String(), notificationId]);
}
Future<List<Map<String, dynamic>>> getPendingNotifications() async {
Database db = await database;
return await db.query(
notificationTrackingTable,
where: 'status IN (?, ?)',
whereArgs: ['pending', 'retrying'],
);
}
Future<void> markNotificationExpired(int notificationId) async {
Database db = await database;
await db.update(
notificationTrackingTable,
{'status': 'expired'},
where: 'notificationId = ?',
whereArgs: [notificationId],
);
}
Future<void> cleanupOldNotificationTracking() async {
Database db = await database;
// Remove tracking records older than 7 days
final cutoffDate = DateTime.now().subtract(const Duration(days: 7)).toIso8601String();
await db.delete(
notificationTrackingTable,
where: 'createdAt < ?',
whereArgs: [cutoffDate],
);
}
Future<void> clearNotificationTracking(int supplementId) async {
Database db = await database;
await db.delete(
notificationTrackingTable,
where: 'supplementId = ?',
whereArgs: [supplementId],
);
}
}