From 666008f05d8adf949fab5b570eaac4c484034422 Mon Sep 17 00:00:00 2001 From: Menno van Leeuwen Date: Sun, 31 Aug 2025 19:15:32 +0200 Subject: [PATCH] Refactor and enhance UI components across multiple screens - Updated date and time formatting in debug notifications screen for clarity. - Wrapped context-dependent state updates in post-frame callbacks in history screen to ensure proper context usage. - Improved layout and styling in settings screen by reordering radio list tiles. - Enhanced logging in auto sync service for better error tracking. - Added context mounted checks in notification router to prevent errors during navigation. - Updated bulk take dialog to use new UI components from shadcn_ui package. - Refactored take supplement dialog to utilize shadcn_ui for a more modern look and feel. - Adjusted info chip and supplement card widgets to use updated color schemes and layouts. - Updated pubspec.yaml and pubspec.lock to include new dependencies and versions. --- .kilocode/mcp.json | 14 + lib/main.dart | 21 +- lib/screens/add_supplement_screen.dart | 4 +- lib/screens/archived_supplements_screen.dart | 4 +- lib/screens/debug_notifications_screen.dart | 4 +- lib/screens/history_screen.dart | 128 ++++- lib/screens/settings_screen.dart | 12 +- lib/services/auto_sync_service.dart | 2 +- lib/services/notification_router.dart | 16 +- lib/widgets/dialogs/bulk_take_dialog.dart | 2 +- .../dialogs/take_supplement_dialog.dart | 440 ++++++++---------- lib/widgets/info_chip.dart | 3 +- lib/widgets/supplement_card.dart | 232 ++++----- pubspec.lock | 135 +++++- pubspec.yaml | 3 + 15 files changed, 597 insertions(+), 423 deletions(-) create mode 100644 .kilocode/mcp.json diff --git a/.kilocode/mcp.json b/.kilocode/mcp.json new file mode 100644 index 0000000..593d07e --- /dev/null +++ b/.kilocode/mcp.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "context7": { + "command": "npx", + "args": [ + "-y", + "@upstash/context7-mcp" + ], + "env": { + "DEFAULT_MINIMUM_TOKENS": "" + } + } + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index f4c8784..d1a3076 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; // Import this import 'package:supplements/logging.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import 'providers/settings_provider.dart'; import 'providers/simple_sync_provider.dart'; @@ -92,22 +93,16 @@ class MyApp extends StatelessWidget { }); }); - return MaterialApp( + return ShadApp( navigatorKey: navigatorKey, title: 'Supplements Tracker', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blue, - brightness: Brightness.light, - ), - useMaterial3: true, + theme: ShadThemeData( + brightness: Brightness.light, + colorScheme: const ShadBlueColorScheme.light(), ), - darkTheme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blue, - brightness: Brightness.dark, - ), - useMaterial3: true, + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadBlueColorScheme.dark(), ), themeMode: settingsProvider.themeMode, home: const HomeScreen(), diff --git a/lib/screens/add_supplement_screen.dart b/lib/screens/add_supplement_screen.dart index 35af991..c673558 100644 --- a/lib/screens/add_supplement_screen.dart +++ b/lib/screens/add_supplement_screen.dart @@ -166,7 +166,7 @@ class _AddSupplementScreenState extends State { Expanded( flex: 1, child: DropdownButtonFormField( - value: controller.selectedUnit, + initialValue: controller.selectedUnit, decoration: const InputDecoration( labelText: 'Unit', border: OutlineInputBorder(), @@ -317,7 +317,7 @@ class _AddSupplementScreenState extends State { Expanded( flex: 1, child: DropdownButtonFormField( - value: _selectedUnitType, + initialValue: _selectedUnitType, decoration: const InputDecoration( labelText: 'Type', border: OutlineInputBorder(), diff --git a/lib/screens/archived_supplements_screen.dart b/lib/screens/archived_supplements_screen.dart index e0cc2ea..848162a 100644 --- a/lib/screens/archived_supplements_screen.dart +++ b/lib/screens/archived_supplements_screen.dart @@ -164,7 +164,7 @@ class _ArchivedSupplementCard extends StatelessWidget { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: Theme.of(context).colorScheme.surfaceVariant.withValues(alpha: 0.3), + color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), border: Border.all( color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2), width: 1, @@ -262,7 +262,7 @@ class _ArchivedSupplementCard extends StatelessWidget { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant.withValues(alpha: 0.3), + color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(8), ), child: Column( diff --git a/lib/screens/debug_notifications_screen.dart b/lib/screens/debug_notifications_screen.dart index 96941a0..15e1eb3 100644 --- a/lib/screens/debug_notifications_screen.dart +++ b/lib/screens/debug_notifications_screen.dart @@ -383,8 +383,8 @@ class _DebugNotificationsScreenState extends State { final createdAt = DateTime.fromMillisecondsSinceEpoch(e.createdAtEpochMs).toLocal(); // Format times more clearly - final scheduledStr = '${scheduledAt.toString().substring(0, 16)}'; - final createdStr = '${createdAt.toString().substring(0, 16)}'; + final scheduledStr = scheduledAt.toString().substring(0, 16); + final createdStr = createdAt.toString().substring(0, 16); // Show status and timing info final statusStr = inQueue ? '🟡 Pending' : '✅ Completed/Canceled'; diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index fda7e2b..369596c 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -55,7 +55,9 @@ class _HistoryScreenState extends State { _selectedMonth--; } }); - context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); + }); }, icon: const Icon(Icons.chevron_left), ), @@ -85,7 +87,9 @@ class _HistoryScreenState extends State { _selectedMonth++; } }); - context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); + }); } }, icon: const Icon(Icons.chevron_right), @@ -187,12 +191,15 @@ class _HistoryScreenState extends State { ); if (picked != null) { + if (!context.mounted) return; setState(() { _selectedMonth = picked.month; _selectedYear = picked.year; _selectedDay = picked; // Set the selected day to the picked date }); - context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); + }); } } @@ -210,13 +217,16 @@ class _HistoryScreenState extends State { ElevatedButton( onPressed: () async { await context.read().deleteIntake(intakeId); + if (!context.mounted) return; Navigator.of(context).pop(); // Force refresh of the UI setState(() {}); // Force refresh of the current view data - context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); + }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -252,7 +262,7 @@ class _HistoryScreenState extends State { final calendarHeight = isWideScreen ? 400.0 : calendarContentHeight; return Card( - child: Container( + child: SizedBox( height: calendarHeight, child: Padding( padding: const EdgeInsets.all(16), @@ -261,20 +271,98 @@ class _HistoryScreenState extends State { children: [ // Calendar header (weekdays) Row( - children: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - .map((day) => Expanded( - child: Center( - child: Text( - day, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: isWideScreen ? 14 : 12, - ), - ), - ), - )) - .toList(), + children: [ + Expanded( + child: Center( + child: Text( + 'Mon', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: isWideScreen ? 14 : 12, + ), + ), + ), + ), + SizedBox(width: 4), + Expanded( + child: Center( + child: Text( + 'Tue', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: isWideScreen ? 14 : 12, + ), + ), + ), + ), + SizedBox(width: 4), + Expanded( + child: Center( + child: Text( + 'Wed', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: isWideScreen ? 14 : 12, + ), + ), + ), + ), + SizedBox(width: 4), + Expanded( + child: Center( + child: Text( + 'Thu', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: isWideScreen ? 14 : 12, + ), + ), + ), + ), + SizedBox(width: 4), + Expanded( + child: Center( + child: Text( + 'Fri', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: isWideScreen ? 14 : 12, + ), + ), + ), + ), + SizedBox(width: 4), + Expanded( + child: Center( + child: Text( + 'Sat', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: isWideScreen ? 14 : 12, + ), + ), + ), + ), + SizedBox(width: 4), + Expanded( + child: Center( + child: Text( + 'Sun', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: isWideScreen ? 14 : 12, + ), + ), + ), + ), + ], ), const SizedBox(height: 8), // Calendar grid @@ -293,7 +381,7 @@ class _HistoryScreenState extends State { final dayNumber = index - firstWeekday + 2; if (dayNumber < 1 || dayNumber > daysInMonth) { - return const SizedBox(); // Empty cell + return const SizedBox.shrink(); // Empty cell } final date = DateTime(_selectedYear, _selectedMonth, dayNumber); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 7762471..77accc4 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -67,8 +67,6 @@ class SettingsScreen extends StatelessWidget { ), const SizedBox(height: 16), RadioListTile( - title: const Text('Follow System'), - subtitle: const Text('Use system theme setting'), value: ThemeOption.system, groupValue: settingsProvider.themeOption, onChanged: (value) { @@ -76,10 +74,10 @@ class SettingsScreen extends StatelessWidget { settingsProvider.setThemeOption(value); } }, + title: const Text('Follow System'), + subtitle: const Text('Use system theme setting'), ), RadioListTile( - title: const Text('Light Theme'), - subtitle: const Text('Always use light theme'), value: ThemeOption.light, groupValue: settingsProvider.themeOption, onChanged: (value) { @@ -87,10 +85,10 @@ class SettingsScreen extends StatelessWidget { settingsProvider.setThemeOption(value); } }, + title: const Text('Light Theme'), + subtitle: const Text('Always use light theme'), ), RadioListTile( - title: const Text('Dark Theme'), - subtitle: const Text('Always use dark theme'), value: ThemeOption.dark, groupValue: settingsProvider.themeOption, onChanged: (value) { @@ -98,6 +96,8 @@ class SettingsScreen extends StatelessWidget { settingsProvider.setThemeOption(value); } }, + title: const Text('Dark Theme'), + subtitle: const Text('Always use dark theme'), ), ], ), diff --git a/lib/services/auto_sync_service.dart b/lib/services/auto_sync_service.dart index d66907e..2943552 100644 --- a/lib/services/auto_sync_service.dart +++ b/lib/services/auto_sync_service.dart @@ -232,7 +232,7 @@ class AutoSyncService { if (_consecutiveFailures >= _autoDisableThreshold) { _autoDisabledDueToErrors = true; if (kDebugMode) { - printLog('AutoSyncService: Auto-sync disabled due to ${_consecutiveFailures} consecutive failures'); + printLog('AutoSyncService: Auto-sync disabled due to $_consecutiveFailures consecutive failures'); } // For configuration errors, disable immediately diff --git a/lib/services/notification_router.dart b/lib/services/notification_router.dart index 8522baa..bfa6a80 100644 --- a/lib/services/notification_router.dart +++ b/lib/services/notification_router.dart @@ -93,6 +93,7 @@ class NotificationRouter { } final context = _navigatorKey!.currentContext!; + if (!context.mounted) return; final provider = context.read(); if (payload == null) { @@ -113,6 +114,7 @@ class NotificationRouter { if (s == null) { // Attempt reload once await provider.loadSupplements(); + if (!context.mounted) return; try { s = provider.supplements.firstWhere((el) => el.id == id); } catch (_) { @@ -124,6 +126,7 @@ class NotificationRouter { // Ensure we close any existing dialog first _popAnyDialog(context); await showTakeSupplementDialog(context, s, hideTime: false); + if (!context.mounted) return; } else { printLog('⚠️ Supplement id=$id not found for single-take routing'); _showSnack(context, 'Supplement not found'); @@ -145,6 +148,7 @@ class NotificationRouter { _popAnyDialog(context); await showBulkTakeDialog(context, list); + if (!context.mounted) return; } } else { printLog('⚠️ Unknown payload type: $type'); @@ -158,8 +162,9 @@ class NotificationRouter { } // Try to wait for providers to be ready to build rich content. - final ready = await _waitUntilReady(timeout: const Duration(seconds: 5)); BuildContext? ctx = _navigatorKey?.currentContext; + final ready = await _waitUntilReady(timeout: const Duration(seconds: 5)); + if (ctx != null && !ctx.mounted) ctx = null; SupplementProvider? provider; if (ready && ctx != null) { @@ -262,6 +267,7 @@ class NotificationRouter { final key = _navigatorKey; final ctx = key?.currentContext; if (ctx != null) { + if (!ctx.mounted) continue; try { final provider = Provider.of(ctx, listen: false); if (!provider.isLoading) { @@ -283,8 +289,10 @@ class NotificationRouter { } void _showSnack(BuildContext context, String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + }); } } diff --git a/lib/widgets/dialogs/bulk_take_dialog.dart b/lib/widgets/dialogs/bulk_take_dialog.dart index 34703da..5c78f95 100644 --- a/lib/widgets/dialogs/bulk_take_dialog.dart +++ b/lib/widgets/dialogs/bulk_take_dialog.dart @@ -51,7 +51,7 @@ Future showBulkTakeDialog( return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant.withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), diff --git a/lib/widgets/dialogs/take_supplement_dialog.dart b/lib/widgets/dialogs/take_supplement_dialog.dart index a68a080..9d56265 100644 --- a/lib/widgets/dialogs/take_supplement_dialog.dart +++ b/lib/widgets/dialogs/take_supplement_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../models/supplement.dart'; import '../../providers/supplement_provider.dart'; @@ -16,260 +17,227 @@ Future showTakeSupplementDialog( DateTime selectedDateTime = DateTime.now(); bool useCustomTime = false; - await showDialog( + await showShadDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setState) { - return AlertDialog( + return ShadDialog( title: Text('Take ${supplement.name}'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: TextField( - controller: unitsController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - decoration: InputDecoration( - labelText: 'Number of ${supplement.unitType}', - border: const OutlineInputBorder(), - suffixText: supplement.unitType, - ), - onChanged: (value) => setState(() {}), - ), - ), - ], - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, - borderRadius: BorderRadius.circular(4), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( children: [ - Text( - 'Total dosage:', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - Text( - supplement.ingredientsDisplay, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, + Expanded( + child: ShadInput( + controller: unitsController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + placeholder: Text('Number of ${supplement.unitType}'), + onChanged: (value) => setState(() {}), ), ), ], ), - ), - const SizedBox(height: 16), - - if (!hideTime) ...[ - // Time selection section - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.access_time, - size: 16, - color: Theme.of(context).colorScheme.primary, + const SizedBox(height: 8), + ShadCard( + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total dosage:', + style: TextStyle( + fontSize: 12, + color: ShadTheme.of(context).colorScheme.foreground, ), - 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( - 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( - 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.withValues(alpha: 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)), - ), - ], - ), - ], + ), + Text( + supplement.ingredientsDisplay, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: ShadTheme.of(context).colorScheme.foreground, ), ), ], - ], + ), ), ), const SizedBox(height: 16), - ], - - TextField( - controller: notesController, - decoration: const InputDecoration( - labelText: 'Notes (optional)', - border: OutlineInputBorder(), - ), - maxLines: 2, - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () { - final unitsTaken = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble(); - // 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().recordIntake( - supplement.id!, - totalDosageTaken, - unitsTaken: unitsTaken, - notes: notesController.text.isNotEmpty ? notesController.text : null, - takenAt: hideTime - ? null - : (useCustomTime ? selectedDateTime : null), - ); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${supplement.name} recorded!'), - backgroundColor: Colors.green, + if (!hideTime) ...[ + ShadCard( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.access_time, + size: 16, + color: ShadTheme.of(context).colorScheme.primary, + ), + const SizedBox(width: 6), + Text( + 'When did you take it?', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: ShadTheme.of(context).colorScheme.primary, + ), + ), + ], + ), + const SizedBox(height: 8), + ShadRadioGroupFormField( + initialValue: useCustomTime, + onChanged: (value) => setState(() => useCustomTime = value!), + items: [ + ShadRadio( + value: false, + label: const Text('Just now', style: TextStyle(fontSize: 12)), + ), + ShadRadio( + value: true, + label: const Text('Custom time', style: TextStyle(fontSize: 12)), + ), + ], + ), + if (useCustomTime) ...[ + const SizedBox(height: 8), + ShadCard( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: ShadTheme.of(context).colorScheme.foreground, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Date: ${selectedDateTime.day}/${selectedDateTime.month}/${selectedDateTime.year}', + style: const TextStyle(fontSize: 12), + ), + ), + ShadButton.outline( + 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), + Row( + children: [ + Icon( + Icons.access_time, + size: 14, + color: ShadTheme.of(context).colorScheme.foreground, + ), + 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), + ), + ), + ShadButton.outline( + 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)), + ), + ], + ), + ], + ), + ), + ), + ], + ], + ), + ), ), - ); - }, - child: const Text('Record'), + const SizedBox(height: 16), + ], + ShadInput( + controller: notesController, + placeholder: const Text('Notes (optional)'), + maxLines: 2, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ShadButton.outline( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + const SizedBox(width: 8), + ShadButton( + onPressed: () { + final unitsTaken = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble(); + final totalDosageTaken = 0.0; + context.read().recordIntake( + supplement.id!, + totalDosageTaken, + unitsTaken: unitsTaken, + notes: notesController.text.isNotEmpty ? notesController.text : null, + takenAt: hideTime ? null : (useCustomTime ? selectedDateTime : null), + ); + Navigator.of(context).pop(); + ShadToaster.of(context).show( + ShadToast( + title: Text('${supplement.name} recorded!'), + ), + ); + }, + child: const Text('Record'), + ), + ], + ), + ], ), - ], + ), ); }, ), diff --git a/lib/widgets/info_chip.dart b/lib/widgets/info_chip.dart index c518eae..3636218 100644 --- a/lib/widgets/info_chip.dart +++ b/lib/widgets/info_chip.dart @@ -7,6 +7,7 @@ class InfoChip extends StatelessWidget { final bool fullWidth; const InfoChip({ + super.key, required this.icon, required this.label, required this.context, @@ -19,7 +20,7 @@ class InfoChip extends StatelessWidget { width: fullWidth ? double.infinity : null, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant.withValues(alpha: 0.4), + color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(8), ), child: Row( diff --git a/lib/widgets/supplement_card.dart b/lib/widgets/supplement_card.dart index 5500ff3..37cf6f1 100644 --- a/lib/widgets/supplement_card.dart +++ b/lib/widgets/supplement_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../models/supplement.dart'; import '../providers/supplement_provider.dart'; @@ -56,10 +57,10 @@ class _SupplementCardState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: isCompletelyTaken - ? Theme.of(context).colorScheme.surface - : isTakenToday - ? Theme.of(context).colorScheme.secondaryContainer - : Theme.of(context).colorScheme.surface, + ? Theme.of(context).colorScheme.surface + : isTakenToday + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.surface, border: Border.all( color: isCompletelyTaken ? Colors.green.shade600 @@ -176,24 +177,6 @@ class _SupplementCardState extends State { ), ), ), - ElevatedButton( - onPressed: widget.onTake, - style: ElevatedButton.styleFrom( - backgroundColor: isCompletelyTaken - ? Colors.green.shade500 - : Theme.of(context).colorScheme.primary, - foregroundColor: Theme.of(context).colorScheme.inverseSurface, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - minimumSize: const Size(60, 32), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Text( - isCompletelyTaken ? '✓' : 'Take', - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), - ), - ), ], ], ], @@ -223,18 +206,17 @@ class _SupplementCardState extends State { } }, itemBuilder: (context) => [ - if (isTakenToday) - PopupMenuItem( - value: 'take', - onTap: widget.onTake, - child: const Row( - children: [ - Icon(Icons.add_circle_outline), - SizedBox(width: 8), - Text('Take Again'), - ], - ), + PopupMenuItem( + value: 'take', + onTap: widget.onTake, + child: Row( + children: [ + Icon(isTakenToday ? Icons.add_circle_outline : Icons.medication), + const SizedBox(width: 8), + Text(isTakenToday ? 'Take Again' : 'Take'), + ], ), + ), const PopupMenuItem( value: 'edit', child: Row( @@ -325,7 +307,7 @@ class _SupplementCardState extends State { children: todayIntakes.map((intake) { final units = intake['units'] as double; final unitsText = units == 1.0 - ? '${widget.supplement.unitType}' + ? widget.supplement.unitType : '${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${widget.supplement.unitType}'; return Container( @@ -356,49 +338,38 @@ class _SupplementCardState extends State { ], // Ingredients section - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ingredients per ${widget.supplement.unitType}:', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurfaceVariant, + ShadCard( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ingredients per ${widget.supplement.unitType}:', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: ShadTheme.of(context).colorScheme.foreground, + ), ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 4, - children: widget.supplement.ingredients.map((ingredient) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 4, + children: widget.supplement.ingredients.map((ingredient) { + return ShadBadge( + child: Text( + '${ingredient.name} ${ingredient.amount}${ingredient.unit}', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + ), ), - ), - child: Text( - '${ingredient.name} ${ingredient.amount}${ingredient.unit}', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary, - ), - ), - ); - }).toList(), - ), - ], + ); + }).toList(), + ), + ], + ), ), ), @@ -408,18 +379,34 @@ class _SupplementCardState extends State { Row( children: [ Expanded( - child: _InfoChip( - icon: Icons.schedule, - label: '${widget.supplement.frequencyPerDay}x daily', - context: context, + child: ShadBadge( + child: Row( + children: [ + Icon( + Icons.schedule, + size: 14, + color: ShadTheme.of(context).colorScheme.foreground, + ), + const SizedBox(width: 4), + Text('${widget.supplement.frequencyPerDay}x daily'), + ], + ), ), ), const SizedBox(width: 8), Expanded( - child: _InfoChip( - icon: Icons.medication, - label: '${widget.supplement.numberOfUnits} ${widget.supplement.unitType}', - context: context, + child: ShadBadge( + child: Row( + children: [ + Icon( + Icons.medication, + size: 14, + color: ShadTheme.of(context).colorScheme.foreground, + ), + const SizedBox(width: 4), + Text('${widget.supplement.numberOfUnits} ${widget.supplement.unitType}'), + ], + ), ), ), ], @@ -427,66 +414,43 @@ class _SupplementCardState extends State { if (widget.supplement.reminderTimes.isNotEmpty) ...[ const SizedBox(height: 8), - _InfoChip( - icon: Icons.notifications, - label: 'Reminders: ${widget.supplement.reminderTimes.join(', ')}', - context: context, - fullWidth: true, + ShadBadge( + child: Row( + children: [ + Icon( + Icons.notifications, + size: 14, + color: ShadTheme.of(context).colorScheme.foreground, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + 'Reminders: ${widget.supplement.reminderTimes.join(', ')}', + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ), ], if (widget.supplement.notes != null && widget.supplement.notes!.isNotEmpty) ...[ const SizedBox(height: 8), - Container( - width: double.infinity, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - widget.supplement.notes!, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontStyle: FontStyle.italic, + ShadCard( + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + widget.supplement.notes!, + style: TextStyle( + fontSize: 12, + color: ShadTheme.of(context).colorScheme.foreground, + fontStyle: FontStyle.italic, + ), ), ), ), ], - const SizedBox(height: 16), - - // Take button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: widget.onTake, - icon: Icon( - isCompletelyTaken ? Icons.check_circle : Icons.medication, - size: 18, - ), - label: Text( - isCompletelyTaken - ? 'All doses taken today' - : isTakenToday - ? 'Take next dose' - : 'Take supplement', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - style: ElevatedButton.styleFrom( - backgroundColor: isCompletelyTaken - ? Colors.green.shade500 - : Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: isCompletelyTaken ? 0 : 2, - ), - ), - ), ], ), ), @@ -516,7 +480,7 @@ class _InfoChip extends StatelessWidget { width: fullWidth ? double.infinity : null, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant.withValues(alpha: 0.4), + color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(8), ), child: Row( diff --git a/pubspec.lock b/pubspec.lock index 1739804..41c6664 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + boxy: + dependency: transitive + description: + name: boxy + sha256: "71af0cd1bf7889c09787f26219a345aa4f38ccb98384c8ec24189e4d8e746005" + url: "https://pub.dev" + source: hosted + version: "2.2.1" characters: dependency: transitive description: @@ -129,6 +137,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.7" + extended_image: + dependency: transitive + description: + name: extended_image + sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 + url: "https://pub.dev" + source: hosted + version: "10.0.1" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" + url: "https://pub.dev" + source: hosted + version: "5.0.1" fake_async: dependency: transitive description: @@ -166,6 +190,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: transitive + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" flutter_fgbg: dependency: transitive description: @@ -214,6 +246,11 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_secure_storage: dependency: "direct main" description: @@ -263,6 +300,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 + url: "https://pub.dev" + source: hosted + version: "2.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -281,6 +334,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" + url: "https://pub.dev" + source: hosted + version: "3.0.0" http_parser: dependency: transitive description: @@ -297,6 +358,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" json_annotation: dependency: transitive description: @@ -345,6 +414,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + lucide_icons_flutter: + dependency: transitive + description: + name: lucide_icons_flutter + sha256: c88e3611c0aa272ca2f2aa263662174ae4996f5e3ee1c300021514df230b6588 + url: "https://pub.dev" + source: hosted + version: "3.0.9" matcher: dependency: transitive description: @@ -401,6 +478,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: transitive description: @@ -489,6 +574,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + shadcn_ui: + dependency: "direct main" + description: + name: shadcn_ui + sha256: "628d1a7f36e4c764dae3b86b38abb086adf39ccea0073960f481777d1880f90d" + url: "https://pub.dev" + source: hosted + version: "0.29.2" shared_preferences: dependency: "direct main" description: @@ -678,6 +771,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.1" + two_dimensional_scrollables: + dependency: transitive + description: + name: two_dimensional_scrollables + sha256: "0f77ecb96596f2f82eec2b0a8e60d9305c58315557da9fa3b610c7dbf5ded621" + url: "https://pub.dev" + source: hosted + version: "0.3.7" typed_data: dependency: transitive description: @@ -686,6 +787,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_image: + dependency: transitive + description: + name: universal_image + sha256: ef47a4a002158cf0b36ed3b7605af132d2476cc42703e41b8067d3603705c40d + url: "https://pub.dev" + source: hosted + version: "1.0.11" url_launcher: dependency: "direct main" description: @@ -758,6 +867,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" vector_math: dependency: transitive description: @@ -816,4 +949,4 @@ packages: version: "6.6.1" sdks: dart: ">=3.9.0 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2f383a1..9fa3819 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,9 @@ dependencies: # Date time handling intl: ^0.20.2 + # UI components + shadcn_ui: ^0.29.2 + # WebDAV sync functionality webdav_client: ^1.2.2 connectivity_plus: ^6.1.5