From 769d113713cc1ee80f35e337ae2e8d03a422ee63 Mon Sep 17 00:00:00 2001 From: Menno van Leeuwen Date: Tue, 9 Sep 2025 16:22:55 +0200 Subject: [PATCH] feat: Integrate ShadSonner for improved toast notifications across multiple screens --- .kilocode/mcp.json | 6 +- android/app/src/main/AndroidManifest.xml | 1 + lib/main.dart | 4 +- lib/screens/add_supplement_screen.dart | 47 +++++++--- lib/screens/archived_supplements_screen.dart | 15 ++-- lib/screens/debug_notifications_screen.dart | 31 ++++--- lib/screens/history_screen.dart | 18 ++-- lib/screens/settings_screen.dart | 18 +++- lib/screens/simple_sync_settings_screen.dart | 94 +++++++++++++++----- lib/screens/supplements_list_screen.dart | 15 ++-- lib/services/notification_router.dart | 15 ++-- lib/widgets/dialogs/bulk_take_dialog.dart | 16 +++- 12 files changed, 197 insertions(+), 83 deletions(-) diff --git a/.kilocode/mcp.json b/.kilocode/mcp.json index 593d07e..ae1e5f9 100644 --- a/.kilocode/mcp.json +++ b/.kilocode/mcp.json @@ -8,7 +8,11 @@ ], "env": { "DEFAULT_MINIMUM_TOKENS": "" - } + }, + "alwaysAllow": [ + "resolve-library-id", + "get-library-docs" + ] } } } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2fa143c..3aff5a7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + diff --git a/lib/main.dart b/lib/main.dart index 0264bb1..9ee5bb0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -105,7 +105,9 @@ class MyApp extends StatelessWidget { colorScheme: const ShadZincColorScheme.dark(), ), themeMode: settingsProvider.themeMode, - home: const HomeScreen(), + home: ShadSonner( + child: const HomeScreen(), + ), debugShowCheckedModeBanner: false, ); }, diff --git a/lib/screens/add_supplement_screen.dart b/lib/screens/add_supplement_screen.dart index c673558..bdddcd9 100644 --- a/lib/screens/add_supplement_screen.dart +++ b/lib/screens/add_supplement_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:uuid/uuid.dart'; import '../models/ingredient.dart'; @@ -571,10 +572,17 @@ class _AddSupplementScreenState extends State { .toList(); if (validIngredients.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: - Text('Please add at least one ingredient with name and amount'), + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: const Text('Please add at least one ingredient with name and amount'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); return; @@ -611,21 +619,36 @@ class _AddSupplementScreenState extends State { if (mounted) { Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(widget.supplement != null + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: Text(widget.supplement != null ? 'Supplement updated successfully!' : 'Supplement added successfully!'), - backgroundColor: Colors.green, + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: const Text('Error'), + description: Text('${e.toString()}'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); } diff --git a/lib/screens/archived_supplements_screen.dart b/lib/screens/archived_supplements_screen.dart index 848162a..febd687 100644 --- a/lib/screens/archived_supplements_screen.dart +++ b/lib/screens/archived_supplements_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:supplements/widgets/info_chip.dart'; import '../models/supplement.dart'; @@ -97,10 +98,9 @@ class _ArchivedSupplementsScreenState extends State { onPressed: () { context.read().unarchiveSupplement(supplement.id!); Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${supplement.name} unarchived'), - backgroundColor: Colors.green, + ShadSonner.of(context).show( + ShadToast( + title: Text('${supplement.name} unarchived'), ), ); }, @@ -128,10 +128,9 @@ class _ArchivedSupplementsScreenState extends State { onPressed: () { context.read().deleteArchivedSupplement(supplement.id!); Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${supplement.name} deleted permanently'), - backgroundColor: Colors.red, + ShadSonner.of(context).show( + ShadToast( + title: Text('${supplement.name} deleted permanently'), ), ); }, diff --git a/lib/screens/debug_notifications_screen.dart b/lib/screens/debug_notifications_screen.dart index 15e1eb3..2c9e6b0 100644 --- a/lib/screens/debug_notifications_screen.dart +++ b/lib/screens/debug_notifications_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:supplements/providers/supplement_provider.dart'; import 'package:supplements/services/notification_debug_store.dart'; import 'package:supplements/services/simple_notification_service.dart'; @@ -168,8 +169,10 @@ class _DebugNotificationsScreenState extends State { await SimpleNotificationService.instance.cancelById(id); await _load(); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Canceled notification $id')), + ShadSonner.of(context).show( + ShadToast( + title: Text('Canceled notification $id'), + ), ); } @@ -177,8 +180,10 @@ class _DebugNotificationsScreenState extends State { await SimpleNotificationService.instance.cancelAll(); await _load(); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Canceled all notifications')), + ShadSonner.of(context).show( + const ShadToast( + title: Text('Canceled all notifications'), + ), ); } @@ -186,15 +191,19 @@ class _DebugNotificationsScreenState extends State { await NotificationDebugStore.instance.clear(); await _load(); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Cleared debug log')), + ShadSonner.of(context).show( + const ShadToast( + title: Text('Cleared debug log'), + ), ); } void _copyToClipboard(String text) { Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Copied to clipboard')), + ShadSonner.of(context).show( + const ShadToast( + title: Text('Copied to clipboard'), + ), ); } @@ -280,8 +289,10 @@ class _DebugNotificationsScreenState extends State { isSingle: true, // This is a single notification ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Test snooze notification sent!')), + ShadSonner.of(context).show( + const ShadToast( + title: Text('Test snooze notification sent!'), + ), ); }, child: const Text('Send Test Snooze Notification'), diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index ac1b370..e076443 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; -import '../providers/settings_provider.dart'; import '../providers/supplement_provider.dart'; class HistoryScreen extends StatefulWidget { @@ -350,10 +349,17 @@ class _HistoryScreenState extends State { context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('$supplementName intake deleted'), - backgroundColor: Colors.red, + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: Text('$supplementName intake deleted'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); }, diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c4a3920..8e319ab 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../providers/settings_provider.dart'; import 'debug_notifications_screen.dart'; @@ -124,6 +125,7 @@ class SettingsScreen extends StatelessWidget { trailing: DropdownButton( value: settingsProvider.snoozeMinutes, items: const [ + DropdownMenuItem(value: 2, child: Text('2 min')), DropdownMenuItem(value: 5, child: Text('5 min')), DropdownMenuItem(value: 10, child: Text('10 min')), DropdownMenuItem(value: 15, child: Text('15 min')), @@ -301,10 +303,18 @@ class SettingsScreen extends StatelessWidget { ); } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Invalid time ranges: ${e.toString()}'), - backgroundColor: Colors.red, + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: const Text('Invalid time ranges'), + description: Text('${e.toString()}'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); } diff --git a/lib/screens/simple_sync_settings_screen.dart b/lib/screens/simple_sync_settings_screen.dart index 6667634..6cf2471 100644 --- a/lib/screens/simple_sync_settings_screen.dart +++ b/lib/screens/simple_sync_settings_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../providers/settings_provider.dart'; import '../providers/simple_sync_provider.dart'; @@ -553,21 +554,36 @@ class _SimpleSyncSettingsScreenState extends State { final success = await syncProvider.testConnection(); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: Text(success ? 'Connection successful!' : 'Connection failed. Check your settings.'), - backgroundColor: success ? Colors.green : Colors.red, + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Connection test failed: $e'), - backgroundColor: Colors.red, + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: const Text('Connection test failed'), + description: Text('$e'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); } @@ -596,19 +612,34 @@ class _SimpleSyncSettingsScreenState extends State { ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Configuration saved successfully!'), - backgroundColor: Colors.green, + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: const Text('Configuration saved successfully!'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to save configuration: $e'), - backgroundColor: Colors.red, + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: const Text('Failed to save configuration'), + description: Text('$e'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); } @@ -622,19 +653,34 @@ class _SimpleSyncSettingsScreenState extends State { await syncProvider.syncDatabase(isAutoSync: false); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Manual sync completed!'), - backgroundColor: Colors.green, + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: const Text('Manual sync completed!'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Manual sync failed: $e'), - backgroundColor: Colors.red, + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: const Text('Manual sync failed'), + description: Text('$e'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); } diff --git a/lib/screens/supplements_list_screen.dart b/lib/screens/supplements_list_screen.dart index d7b9f1e..f6f16ee 100644 --- a/lib/screens/supplements_list_screen.dart +++ b/lib/screens/supplements_list_screen.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/settings_provider.dart'; @@ -308,10 +309,9 @@ class SupplementsListScreen extends StatelessWidget { onPressed: () { context.read().deleteSupplement(supplement.id!); Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${supplement.name} deleted'), - backgroundColor: Colors.red, + ShadSonner.of(context).show( + ShadToast( + title: Text('${supplement.name} deleted'), ), ); }, @@ -338,10 +338,9 @@ class SupplementsListScreen extends StatelessWidget { onPressed: () { context.read().archiveSupplement(supplement.id!); Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${supplement.name} archived'), - backgroundColor: Colors.orange, + ShadSonner.of(context).show( + ShadToast( + title: Text('${supplement.name} archived'), ), ); }, diff --git a/lib/services/notification_router.dart b/lib/services/notification_router.dart index 3515003..e4fb0a0 100644 --- a/lib/services/notification_router.dart +++ b/lib/services/notification_router.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:provider/provider.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supplements/logging.dart'; import 'package:supplements/services/simple_notification_service.dart'; @@ -32,9 +33,6 @@ class NotificationRouter { printLog('🔔 handleNotificationResponse: Received actionId: $actionId'); printLog('🔔 handleNotificationResponse: Decoded payloadMap: $payloadMap'); - // Cancel retry notifications for any interaction (take, snooze, or tap) - await _cancelRetryNotificationsForResponse(payloadMap); - // Handle Snooze actions without surfacing UI if (actionId == 'snooze_single' || actionId == 'snooze_group') { try { @@ -49,6 +47,11 @@ class NotificationRouter { // Default: route to in-app UI for Take actions and normal taps await _routeFromPayload(payloadMap); + + // Cancel retry notifications only for Take actions (not for taps or snoozes) + if (actionId == 'take_single' || actionId == 'take_group') { + await _cancelRetryNotificationsForResponse(payloadMap); + } } Future handleAppLaunchDetails(NotificationAppLaunchDetails? details) async { @@ -294,8 +297,10 @@ class NotificationRouter { void _showSnack(BuildContext context, String message) { WidgetsBinding.instance.addPostFrameCallback((_) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), + ShadSonner.of(context).show( + ShadToast( + title: Text(message), + ), ); }); } diff --git a/lib/widgets/dialogs/bulk_take_dialog.dart b/lib/widgets/dialogs/bulk_take_dialog.dart index 5c78f95..d9008ec 100644 --- a/lib/widgets/dialogs/bulk_take_dialog.dart +++ b/lib/widgets/dialogs/bulk_take_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'; @@ -183,10 +184,17 @@ Future showBulkTakeDialog( Navigator.of(context).pop(); if (recorded > 0) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Recorded $recorded supplement${recorded == 1 ? '' : 's'}'), - backgroundColor: Colors.green, + final sonner = ShadSonner.of(context); + final id = DateTime.now().millisecondsSinceEpoch; + sonner.show( + ShadToast( + id: id, + title: Text('Recorded $recorded supplement${recorded == 1 ? '' : 's'}'), + action: ShadButton( + size: ShadButtonSize.sm, + child: const Text('Dismiss'), + onPressed: () => sonner.hide(id), + ), ), ); }