From e6181add0861281c15958a33e8568921d40b6f83 Mon Sep 17 00:00:00 2001 From: Menno van Leeuwen Date: Tue, 26 Aug 2025 01:35:43 +0200 Subject: [PATCH] Refactor supplement intake handling to support fractional units and update notification service initialization for Linux --- android/app/src/main/AndroidManifest.xml | 8 +- lib/models/supplement_intake.dart | 6 +- lib/providers/supplement_provider.dart | 6 +- lib/screens/history_screen.dart | 106 +++++++++++++++-------- lib/screens/supplements_list_screen.dart | 8 +- lib/services/database_helper.dart | 43 +++++++-- lib/services/notification_service.dart | 4 + 7 files changed, 127 insertions(+), 54 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a1d5093..ee6ddc4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -33,8 +33,12 @@ - - + + diff --git a/lib/models/supplement_intake.dart b/lib/models/supplement_intake.dart index cfce351..3330085 100644 --- a/lib/models/supplement_intake.dart +++ b/lib/models/supplement_intake.dart @@ -3,7 +3,7 @@ class SupplementIntake { final int supplementId; final DateTime takenAt; 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; SupplementIntake({ @@ -32,7 +32,7 @@ class SupplementIntake { supplementId: map['supplementId'], takenAt: DateTime.parse(map['takenAt']), dosageTaken: map['dosageTaken'], - unitsTaken: map['unitsTaken'] ?? 1, // Default for backwards compatibility + unitsTaken: (map['unitsTaken'] ?? 1).toDouble(), // Default for backwards compatibility notes: map['notes'], ); } @@ -42,7 +42,7 @@ class SupplementIntake { int? supplementId, DateTime? takenAt, double? dosageTaken, - int? unitsTaken, + double? unitsTaken, String? notes, }) { return SupplementIntake( diff --git a/lib/providers/supplement_provider.dart b/lib/providers/supplement_provider.dart index acbdd10..c03eb69 100644 --- a/lib/providers/supplement_provider.dart +++ b/lib/providers/supplement_provider.dart @@ -103,13 +103,13 @@ class SupplementProvider with ChangeNotifier { } } - Future recordIntake(int supplementId, double dosage, {int? unitsTaken, String? notes}) async { + Future recordIntake(int supplementId, double dosage, {double? unitsTaken, String? notes}) async { try { final intake = SupplementIntake( supplementId: supplementId, takenAt: DateTime.now(), dosageTaken: dosage, - unitsTaken: unitsTaken ?? 1, + unitsTaken: unitsTaken ?? 1.0, notes: notes, ); @@ -118,7 +118,7 @@ class SupplementProvider with ChangeNotifier { // Show confirmation notification 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( 'Supplement Taken', 'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} ($dosage ${supplement.unit})', diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index 8b53924..0aefa0a 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -120,62 +120,98 @@ class _HistoryScreenState extends State with SingleTickerProvider } final intakes = snapshot.data!; + + // Group intakes by supplement + final Map>> groupedIntakes = {}; + for (final intake in intakes) { + final supplementName = intake['supplementName'] as String; + groupedIntakes.putIfAbsent(supplementName, () => []); + groupedIntakes[supplementName]!.add(intake); + } + return ListView.builder( padding: const EdgeInsets.all(16), - itemCount: intakes.length, + itemCount: groupedIntakes.length, itemBuilder: (context, index) { - final intake = intakes[index]; - final takenAt = DateTime.parse(intake['takenAt']); + final supplementName = groupedIntakes.keys.elementAt(index); + 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( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( + margin: const EdgeInsets.only(bottom: 12), + child: ExpansionTile( leading: CircleAvatar( backgroundColor: Theme.of(context).colorScheme.primary, 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( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('${intake['dosageTaken']} ${intake['supplementUnit']}'), 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, ), ), - if (intake['notes'] != null && intake['notes'].toString().isNotEmpty) - Text( - intake['notes'], - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontStyle: FontStyle.italic, - ), - ), ], ), - trailing: PopupMenuButton( - onSelected: (value) { - 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)), - ], - ), + 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( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + if (intake['notes'] != null && intake['notes'].toString().isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + intake['notes'], + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ); + }).toList(), ), ); }, diff --git a/lib/screens/supplements_list_screen.dart b/lib/screens/supplements_list_screen.dart index 0b08ba5..d7623b7 100644 --- a/lib/screens/supplements_list_screen.dart +++ b/lib/screens/supplements_list_screen.dart @@ -133,7 +133,7 @@ class SupplementsListScreen extends StatelessWidget { context: context, builder: (context) => StatefulBuilder( 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; return AlertDialog( @@ -146,7 +146,7 @@ class SupplementsListScreen extends StatelessWidget { Expanded( child: TextField( controller: unitsController, - keyboardType: TextInputType.number, + keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( labelText: 'Number of ${supplement.unitType}', border: const OutlineInputBorder(), @@ -175,7 +175,7 @@ class SupplementsListScreen extends StatelessWidget { ), ), Text( - '${totalDosage.toStringAsFixed(1)} ${supplement.unit}', + '${totalDosage.toStringAsFixed(totalDosage % 1 == 0 ? 0 : 1)} ${supplement.unit}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, @@ -203,7 +203,7 @@ class SupplementsListScreen extends StatelessWidget { ), ElevatedButton( onPressed: () { - final unitsTaken = int.tryParse(unitsController.text) ?? supplement.numberOfUnits; + final unitsTaken = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble(); final totalDosageTaken = supplement.dosageAmount * unitsTaken; context.read().recordIntake( supplement.id!, diff --git a/lib/services/database_helper.dart b/lib/services/database_helper.dart index cb39e6b..4f70a29 100644 --- a/lib/services/database_helper.dart +++ b/lib/services/database_helper.dart @@ -68,7 +68,7 @@ class DatabaseHelper { supplementId INTEGER NOT NULL, takenAt TEXT NOT NULL, dosageTaken REAL NOT NULL, - unitsTaken INTEGER NOT NULL DEFAULT 1, + unitsTaken REAL NOT NULL DEFAULT 1, notes TEXT, FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id) ) @@ -77,20 +77,49 @@ class DatabaseHelper { Future _onUpgrade(Database db, int oldVersion, int newVersion) async { 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 numberOfUnits INTEGER DEFAULT 1'); 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(''' UPDATE $supplementsTable - SET dosageAmount = dosage, + SET dosageAmount = COALESCE(dosage, 0), numberOfUnits = 1, unitType = 'units' 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(); List> 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 JOIN $supplementsTable s ON i.supplementId = s.id WHERE i.takenAt >= ? AND i.takenAt <= ? @@ -200,7 +229,7 @@ class DatabaseHelper { String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String(); List> 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 JOIN $supplementsTable s ON i.supplementId = s.id WHERE i.takenAt >= ? AND i.takenAt <= ? diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 2fae072..a05cac8 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -19,10 +19,14 @@ class NotificationService { requestBadgePermission: true, requestSoundPermission: true, ); + const LinuxInitializationSettings linuxSettings = LinuxInitializationSettings( + defaultActionName: 'Open notification', + ); const InitializationSettings initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, + linux: linuxSettings, ); await _notifications.initialize(initSettings);