Refactor supplement intake handling to support fractional units and update notification service initialization for Linux

This commit is contained in:
2025-08-26 01:35:43 +02:00
parent 05a9d13164
commit e6181add08
7 changed files with 127 additions and 54 deletions

View File

@@ -33,8 +33,12 @@
</activity> </activity>
<!-- Notification receiver for local notifications --> <!-- Notification receiver for local notifications -->
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" /> <receiver
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"> android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
android:exported="false" />
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/> <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>

View File

@@ -3,7 +3,7 @@ class SupplementIntake {
final int supplementId; final int supplementId;
final DateTime takenAt; final DateTime takenAt;
final double dosageTaken; // Total dosage amount taken final double dosageTaken; // Total dosage amount taken
final int unitsTaken; // Number of units taken final double unitsTaken; // Number of units taken (can be fractional)
final String? notes; final String? notes;
SupplementIntake({ SupplementIntake({
@@ -32,7 +32,7 @@ class SupplementIntake {
supplementId: map['supplementId'], supplementId: map['supplementId'],
takenAt: DateTime.parse(map['takenAt']), takenAt: DateTime.parse(map['takenAt']),
dosageTaken: map['dosageTaken'], dosageTaken: map['dosageTaken'],
unitsTaken: map['unitsTaken'] ?? 1, // Default for backwards compatibility unitsTaken: (map['unitsTaken'] ?? 1).toDouble(), // Default for backwards compatibility
notes: map['notes'], notes: map['notes'],
); );
} }
@@ -42,7 +42,7 @@ class SupplementIntake {
int? supplementId, int? supplementId,
DateTime? takenAt, DateTime? takenAt,
double? dosageTaken, double? dosageTaken,
int? unitsTaken, double? unitsTaken,
String? notes, String? notes,
}) { }) {
return SupplementIntake( return SupplementIntake(

View File

@@ -103,13 +103,13 @@ class SupplementProvider with ChangeNotifier {
} }
} }
Future<void> recordIntake(int supplementId, double dosage, {int? unitsTaken, String? notes}) async { Future<void> recordIntake(int supplementId, double dosage, {double? unitsTaken, String? notes}) async {
try { try {
final intake = SupplementIntake( final intake = SupplementIntake(
supplementId: supplementId, supplementId: supplementId,
takenAt: DateTime.now(), takenAt: DateTime.now(),
dosageTaken: dosage, dosageTaken: dosage,
unitsTaken: unitsTaken ?? 1, unitsTaken: unitsTaken ?? 1.0,
notes: notes, notes: notes,
); );
@@ -118,7 +118,7 @@ class SupplementProvider with ChangeNotifier {
// Show confirmation notification // Show confirmation notification
final supplement = _supplements.firstWhere((s) => s.id == supplementId); final supplement = _supplements.firstWhere((s) => s.id == supplementId);
final unitsText = unitsTaken != null && unitsTaken > 1 ? '$unitsTaken ${supplement.unitType}' : ''; final unitsText = unitsTaken != null && unitsTaken != 1 ? '${unitsTaken.toStringAsFixed(unitsTaken % 1 == 0 ? 0 : 1)} ${supplement.unitType}' : '';
await _notificationService.showInstantNotification( await _notificationService.showInstantNotification(
'Supplement Taken', 'Supplement Taken',
'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} ($dosage ${supplement.unit})', 'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} ($dosage ${supplement.unit})',

View File

@@ -120,62 +120,98 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
} }
final intakes = snapshot.data!; final intakes = snapshot.data!;
// Group intakes by supplement
final Map<String, List<Map<String, dynamic>>> groupedIntakes = {};
for (final intake in intakes) {
final supplementName = intake['supplementName'] as String;
groupedIntakes.putIfAbsent(supplementName, () => []);
groupedIntakes[supplementName]!.add(intake);
}
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: intakes.length, itemCount: groupedIntakes.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final intake = intakes[index]; final supplementName = groupedIntakes.keys.elementAt(index);
final takenAt = DateTime.parse(intake['takenAt']); 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;
}
return Card( return Card(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 12),
child: ListTile( child: ExpansionTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
child: Icon(Icons.medication, color: Theme.of(context).colorScheme.onPrimary), child: Icon(Icons.medication, color: Theme.of(context).colorScheme.onPrimary),
), ),
title: Text(intake['supplementName']), title: Text(
supplementName,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('${intake['dosageTaken']} ${intake['supplementUnit']}'),
Text( Text(
'Taken at ${DateFormat('HH:mm').format(takenAt)}', '${totalDosage.toStringAsFixed(totalDosage % 1 == 0 ? 0 : 1)} ${firstIntake['supplementUnit']} 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' : ''}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
children: supplementIntakes.map((intake) {
final takenAt = DateTime.parse(intake['takenAt']);
final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0;
return ListTile(
contentPadding: const EdgeInsets.only(left: 72, right: 16),
title: Text(
'${(intake['dosageTaken'] as double).toStringAsFixed((intake['dosageTaken'] as double) % 1 == 0 ? 0 : 1)} ${intake['supplementUnit']}',
style: const TextStyle(fontSize: 14),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'} at ${DateFormat('HH:mm').format(takenAt)}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
if (intake['notes'] != null && intake['notes'].toString().isNotEmpty) if (intake['notes'] != null && intake['notes'].toString().isNotEmpty)
Text( Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
intake['notes'], intake['notes'],
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
), ),
), ),
], ],
), ),
trailing: PopupMenuButton( );
onSelected: (value) { }).toList(),
if (value == 'delete') {
_deleteIntake(context, intake['id'], intake['supplementName']);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Delete', style: TextStyle(color: Colors.red)),
],
),
),
],
),
), ),
); );
}, },

View File

@@ -133,7 +133,7 @@ class SupplementsListScreen extends StatelessWidget {
context: context, context: context,
builder: (context) => StatefulBuilder( builder: (context) => StatefulBuilder(
builder: (context, setState) { builder: (context, setState) {
final units = int.tryParse(unitsController.text) ?? supplement.numberOfUnits; final units = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble();
final totalDosage = supplement.dosageAmount * units; final totalDosage = supplement.dosageAmount * units;
return AlertDialog( return AlertDialog(
@@ -146,7 +146,7 @@ class SupplementsListScreen extends StatelessWidget {
Expanded( Expanded(
child: TextField( child: TextField(
controller: unitsController, controller: unitsController,
keyboardType: TextInputType.number, keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Number of ${supplement.unitType}', labelText: 'Number of ${supplement.unitType}',
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
@@ -175,7 +175,7 @@ class SupplementsListScreen extends StatelessWidget {
), ),
), ),
Text( Text(
'${totalDosage.toStringAsFixed(1)} ${supplement.unit}', '${totalDosage.toStringAsFixed(totalDosage % 1 == 0 ? 0 : 1)} ${supplement.unit}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -203,7 +203,7 @@ class SupplementsListScreen extends StatelessWidget {
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
final unitsTaken = int.tryParse(unitsController.text) ?? supplement.numberOfUnits; final unitsTaken = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble();
final totalDosageTaken = supplement.dosageAmount * unitsTaken; final totalDosageTaken = supplement.dosageAmount * unitsTaken;
context.read<SupplementProvider>().recordIntake( context.read<SupplementProvider>().recordIntake(
supplement.id!, supplement.id!,

View File

@@ -68,7 +68,7 @@ class DatabaseHelper {
supplementId INTEGER NOT NULL, supplementId INTEGER NOT NULL,
takenAt TEXT NOT NULL, takenAt TEXT NOT NULL,
dosageTaken REAL NOT NULL, dosageTaken REAL NOT NULL,
unitsTaken INTEGER NOT NULL DEFAULT 1, unitsTaken REAL NOT NULL DEFAULT 1,
notes TEXT, notes TEXT,
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id) FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
) )
@@ -77,20 +77,49 @@ class DatabaseHelper {
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async { Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) { if (oldVersion < 2) {
// Add new columns for version 2 // First, add new columns
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN dosageAmount REAL DEFAULT 0'); await db.execute('ALTER TABLE $supplementsTable ADD COLUMN dosageAmount REAL DEFAULT 0');
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN numberOfUnits INTEGER DEFAULT 1'); await db.execute('ALTER TABLE $supplementsTable ADD COLUMN numberOfUnits INTEGER DEFAULT 1');
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN unitType TEXT DEFAULT "units"'); await db.execute('ALTER TABLE $supplementsTable ADD COLUMN unitType TEXT DEFAULT "units"');
await db.execute('ALTER TABLE $intakesTable ADD COLUMN unitsTaken INTEGER DEFAULT 1'); await db.execute('ALTER TABLE $intakesTable ADD COLUMN unitsTaken REAL DEFAULT 1');
// Migrate existing data // Migrate existing data from old dosage column to new dosageAmount column
await db.execute(''' await db.execute('''
UPDATE $supplementsTable UPDATE $supplementsTable
SET dosageAmount = dosage, SET dosageAmount = COALESCE(dosage, 0),
numberOfUnits = 1, numberOfUnits = 1,
unitType = 'units' unitType = 'units'
WHERE dosageAmount = 0 WHERE dosageAmount = 0
'''); ''');
// Create new table with correct schema
await db.execute('''
CREATE TABLE ${supplementsTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
dosageAmount REAL NOT NULL,
numberOfUnits INTEGER NOT NULL DEFAULT 1,
unit TEXT NOT NULL,
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
)
''');
// 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
FROM $supplementsTable
''');
// Drop old table and rename new table
await db.execute('DROP TABLE $supplementsTable');
await db.execute('ALTER TABLE ${supplementsTable}_new RENAME TO $supplementsTable');
} }
} }
@@ -184,7 +213,7 @@ class DatabaseHelper {
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String(); String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery(''' List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*, s.name as supplementName, s.unit as supplementUnit SELECT i.*, s.name as supplementName, s.unit as supplementUnit, s.unitType as supplementUnitType
FROM $intakesTable i FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ? WHERE i.takenAt >= ? AND i.takenAt <= ?
@@ -200,7 +229,7 @@ class DatabaseHelper {
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String(); String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery(''' List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*, s.name as supplementName, s.unit as supplementUnit SELECT i.*, s.name as supplementName, s.unit as supplementUnit, s.unitType as supplementUnitType
FROM $intakesTable i FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ? WHERE i.takenAt >= ? AND i.takenAt <= ?

View File

@@ -19,10 +19,14 @@ class NotificationService {
requestBadgePermission: true, requestBadgePermission: true,
requestSoundPermission: true, requestSoundPermission: true,
); );
const LinuxInitializationSettings linuxSettings = LinuxInitializationSettings(
defaultActionName: 'Open notification',
);
const InitializationSettings initSettings = InitializationSettings( const InitializationSettings initSettings = InitializationSettings(
android: androidSettings, android: androidSettings,
iOS: iosSettings, iOS: iosSettings,
linux: linuxSettings,
); );
await _notifications.initialize(initSettings); await _notifications.initialize(initSettings);