feat: Integrate ShadSonner for improved toast notifications across multiple screens

This commit is contained in:
2025-09-09 16:22:55 +02:00
parent 5684a197e7
commit 769d113713
12 changed files with 197 additions and 83 deletions

View File

@@ -8,7 +8,11 @@
], ],
"env": { "env": {
"DEFAULT_MINIMUM_TOKENS": "" "DEFAULT_MINIMUM_TOKENS": ""
} },
"alwaysAllow": [
"resolve-library-id",
"get-library-docs"
]
} }
} }
} }

View File

@@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions for notifications --> <!-- Permissions for notifications -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" /> <uses-permission android:name="android.permission.USE_EXACT_ALARM" />

View File

@@ -105,7 +105,9 @@ class MyApp extends StatelessWidget {
colorScheme: const ShadZincColorScheme.dark(), colorScheme: const ShadZincColorScheme.dark(),
), ),
themeMode: settingsProvider.themeMode, themeMode: settingsProvider.themeMode,
home: const HomeScreen(), home: ShadSonner(
child: const HomeScreen(),
),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
); );
}, },

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../models/ingredient.dart'; import '../models/ingredient.dart';
@@ -571,10 +572,17 @@ class _AddSupplementScreenState extends State<AddSupplementScreen> {
.toList(); .toList();
if (validIngredients.isEmpty) { if (validIngredients.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
const SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: sonner.show(
Text('Please add at least one ingredient with name and amount'), 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; return;
@@ -611,21 +619,36 @@ class _AddSupplementScreenState extends State<AddSupplementScreen> {
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text(widget.supplement != null sonner.show(
ShadToast(
id: id,
title: Text(widget.supplement != null
? 'Supplement updated successfully!' ? 'Supplement updated successfully!'
: 'Supplement added successfully!'), : 'Supplement added successfully!'),
backgroundColor: Colors.green, action: ShadButton(
size: ShadButtonSize.sm,
child: const Text('Dismiss'),
onPressed: () => sonner.hide(id),
),
), ),
); );
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text('Error: ${e.toString()}'), sonner.show(
backgroundColor: Colors.red, ShadToast(
id: id,
title: const Text('Error'),
description: Text('${e.toString()}'),
action: ShadButton(
size: ShadButtonSize.sm,
child: const Text('Dismiss'),
onPressed: () => sonner.hide(id),
),
), ),
); );
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:supplements/widgets/info_chip.dart'; import 'package:supplements/widgets/info_chip.dart';
import '../models/supplement.dart'; import '../models/supplement.dart';
@@ -97,10 +98,9 @@ class _ArchivedSupplementsScreenState extends State<ArchivedSupplementsScreen> {
onPressed: () { onPressed: () {
context.read<SupplementProvider>().unarchiveSupplement(supplement.id!); context.read<SupplementProvider>().unarchiveSupplement(supplement.id!);
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
SnackBar( ShadToast(
content: Text('${supplement.name} unarchived'), title: Text('${supplement.name} unarchived'),
backgroundColor: Colors.green,
), ),
); );
}, },
@@ -128,10 +128,9 @@ class _ArchivedSupplementsScreenState extends State<ArchivedSupplementsScreen> {
onPressed: () { onPressed: () {
context.read<SupplementProvider>().deleteArchivedSupplement(supplement.id!); context.read<SupplementProvider>().deleteArchivedSupplement(supplement.id!);
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
SnackBar( ShadToast(
content: Text('${supplement.name} deleted permanently'), title: Text('${supplement.name} deleted permanently'),
backgroundColor: Colors.red,
), ),
); );
}, },

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:supplements/providers/supplement_provider.dart'; import 'package:supplements/providers/supplement_provider.dart';
import 'package:supplements/services/notification_debug_store.dart'; import 'package:supplements/services/notification_debug_store.dart';
import 'package:supplements/services/simple_notification_service.dart'; import 'package:supplements/services/simple_notification_service.dart';
@@ -168,8 +169,10 @@ class _DebugNotificationsScreenState extends State<DebugNotificationsScreen> {
await SimpleNotificationService.instance.cancelById(id); await SimpleNotificationService.instance.cancelById(id);
await _load(); await _load();
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
SnackBar(content: Text('Canceled notification $id')), ShadToast(
title: Text('Canceled notification $id'),
),
); );
} }
@@ -177,8 +180,10 @@ class _DebugNotificationsScreenState extends State<DebugNotificationsScreen> {
await SimpleNotificationService.instance.cancelAll(); await SimpleNotificationService.instance.cancelAll();
await _load(); await _load();
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
const SnackBar(content: Text('Canceled all notifications')), const ShadToast(
title: Text('Canceled all notifications'),
),
); );
} }
@@ -186,15 +191,19 @@ class _DebugNotificationsScreenState extends State<DebugNotificationsScreen> {
await NotificationDebugStore.instance.clear(); await NotificationDebugStore.instance.clear();
await _load(); await _load();
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
const SnackBar(content: Text('Cleared debug log')), const ShadToast(
title: Text('Cleared debug log'),
),
); );
} }
void _copyToClipboard(String text) { void _copyToClipboard(String text) {
Clipboard.setData(ClipboardData(text: text)); Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
const SnackBar(content: Text('Copied to clipboard')), const ShadToast(
title: Text('Copied to clipboard'),
),
); );
} }
@@ -280,8 +289,10 @@ class _DebugNotificationsScreenState extends State<DebugNotificationsScreen> {
isSingle: true, // This is a single notification isSingle: true, // This is a single notification
); );
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
const SnackBar(content: Text('Test snooze notification sent!')), const ShadToast(
title: Text('Test snooze notification sent!'),
),
); );
}, },
child: const Text('Send Test Snooze Notification'), child: const Text('Send Test Snooze Notification'),

View File

@@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../providers/settings_provider.dart';
import '../providers/supplement_provider.dart'; import '../providers/supplement_provider.dart';
class HistoryScreen extends StatefulWidget { class HistoryScreen extends StatefulWidget {
@@ -350,10 +349,17 @@ class _HistoryScreenState extends State<HistoryScreen> {
context.read<SupplementProvider>().loadMonthlyIntakes(_selectedYear, _selectedMonth); context.read<SupplementProvider>().loadMonthlyIntakes(_selectedYear, _selectedMonth);
}); });
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text('$supplementName intake deleted'), sonner.show(
backgroundColor: Colors.red, ShadToast(
id: id,
title: Text('$supplementName intake deleted'),
action: ShadButton(
size: ShadButtonSize.sm,
child: const Text('Dismiss'),
onPressed: () => sonner.hide(id),
),
), ),
); );
}, },

View File

@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../providers/settings_provider.dart'; import '../providers/settings_provider.dart';
import 'debug_notifications_screen.dart'; import 'debug_notifications_screen.dart';
@@ -124,6 +125,7 @@ class SettingsScreen extends StatelessWidget {
trailing: DropdownButton<int>( trailing: DropdownButton<int>(
value: settingsProvider.snoozeMinutes, value: settingsProvider.snoozeMinutes,
items: const [ items: const [
DropdownMenuItem(value: 2, child: Text('2 min')),
DropdownMenuItem(value: 5, child: Text('5 min')), DropdownMenuItem(value: 5, child: Text('5 min')),
DropdownMenuItem(value: 10, child: Text('10 min')), DropdownMenuItem(value: 10, child: Text('10 min')),
DropdownMenuItem(value: 15, child: Text('15 min')), DropdownMenuItem(value: 15, child: Text('15 min')),
@@ -301,10 +303,18 @@ class SettingsScreen extends StatelessWidget {
); );
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text('Invalid time ranges: ${e.toString()}'), sonner.show(
backgroundColor: Colors.red, 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),
),
), ),
); );
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../providers/settings_provider.dart'; import '../providers/settings_provider.dart';
import '../providers/simple_sync_provider.dart'; import '../providers/simple_sync_provider.dart';
@@ -553,21 +554,36 @@ class _SimpleSyncSettingsScreenState extends State<SimpleSyncSettingsScreen> {
final success = await syncProvider.testConnection(); final success = await syncProvider.testConnection();
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text(success sonner.show(
ShadToast(
id: id,
title: Text(success
? 'Connection successful!' ? 'Connection successful!'
: 'Connection failed. Check your settings.'), : '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) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text('Connection test failed: $e'), sonner.show(
backgroundColor: Colors.red, 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<SimpleSyncSettingsScreen> {
); );
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
const SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text('Configuration saved successfully!'), sonner.show(
backgroundColor: Colors.green, ShadToast(
id: id,
title: const Text('Configuration saved successfully!'),
action: ShadButton(
size: ShadButtonSize.sm,
child: const Text('Dismiss'),
onPressed: () => sonner.hide(id),
),
), ),
); );
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text('Failed to save configuration: $e'), sonner.show(
backgroundColor: Colors.red, 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<SimpleSyncSettingsScreen> {
await syncProvider.syncDatabase(isAutoSync: false); await syncProvider.syncDatabase(isAutoSync: false);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
const SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text('Manual sync completed!'), sonner.show(
backgroundColor: Colors.green, ShadToast(
id: id,
title: const Text('Manual sync completed!'),
action: ShadButton(
size: ShadButtonSize.sm,
child: const Text('Dismiss'),
onPressed: () => sonner.hide(id),
),
), ),
); );
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text('Manual sync failed: $e'), sonner.show(
backgroundColor: Colors.red, 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),
),
), ),
); );
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../models/supplement.dart'; import '../models/supplement.dart';
import '../providers/settings_provider.dart'; import '../providers/settings_provider.dart';
@@ -308,10 +309,9 @@ class SupplementsListScreen extends StatelessWidget {
onPressed: () { onPressed: () {
context.read<SupplementProvider>().deleteSupplement(supplement.id!); context.read<SupplementProvider>().deleteSupplement(supplement.id!);
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
SnackBar( ShadToast(
content: Text('${supplement.name} deleted'), title: Text('${supplement.name} deleted'),
backgroundColor: Colors.red,
), ),
); );
}, },
@@ -338,10 +338,9 @@ class SupplementsListScreen extends StatelessWidget {
onPressed: () { onPressed: () {
context.read<SupplementProvider>().archiveSupplement(supplement.id!); context.read<SupplementProvider>().archiveSupplement(supplement.id!);
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
SnackBar( ShadToast(
content: Text('${supplement.name} archived'), title: Text('${supplement.name} archived'),
backgroundColor: Colors.orange,
), ),
); );
}, },

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:supplements/logging.dart'; import 'package:supplements/logging.dart';
import 'package:supplements/services/simple_notification_service.dart'; import 'package:supplements/services/simple_notification_service.dart';
@@ -32,9 +33,6 @@ class NotificationRouter {
printLog('🔔 handleNotificationResponse: Received actionId: $actionId'); printLog('🔔 handleNotificationResponse: Received actionId: $actionId');
printLog('🔔 handleNotificationResponse: Decoded payloadMap: $payloadMap'); printLog('🔔 handleNotificationResponse: Decoded payloadMap: $payloadMap');
// Cancel retry notifications for any interaction (take, snooze, or tap)
await _cancelRetryNotificationsForResponse(payloadMap);
// Handle Snooze actions without surfacing UI // Handle Snooze actions without surfacing UI
if (actionId == 'snooze_single' || actionId == 'snooze_group') { if (actionId == 'snooze_single' || actionId == 'snooze_group') {
try { try {
@@ -49,6 +47,11 @@ class NotificationRouter {
// Default: route to in-app UI for Take actions and normal taps // Default: route to in-app UI for Take actions and normal taps
await _routeFromPayload(payloadMap); 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<void> handleAppLaunchDetails(NotificationAppLaunchDetails? details) async { Future<void> handleAppLaunchDetails(NotificationAppLaunchDetails? details) async {
@@ -294,8 +297,10 @@ class NotificationRouter {
void _showSnack(BuildContext context, String message) { void _showSnack(BuildContext context, String message) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar( ShadSonner.of(context).show(
SnackBar(content: Text(message)), ShadToast(
title: Text(message),
),
); );
}); });
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../models/supplement.dart'; import '../../models/supplement.dart';
import '../../providers/supplement_provider.dart'; import '../../providers/supplement_provider.dart';
@@ -183,10 +184,17 @@ Future<void> showBulkTakeDialog(
Navigator.of(context).pop(); Navigator.of(context).pop();
if (recorded > 0) { if (recorded > 0) {
ScaffoldMessenger.of(context).showSnackBar( final sonner = ShadSonner.of(context);
SnackBar( final id = DateTime.now().millisecondsSinceEpoch;
content: Text('Recorded $recorded supplement${recorded == 1 ? '' : 's'}'), sonner.show(
backgroundColor: Colors.green, ShadToast(
id: id,
title: Text('Recorded $recorded supplement${recorded == 1 ? '' : 's'}'),
action: ShadButton(
size: ShadButtonSize.sm,
child: const Text('Dismiss'),
onPressed: () => sonner.hide(id),
),
), ),
); );
} }