mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 10:27:08 +02:00
feat: Integrate ShadSonner for improved toast notifications across multiple screens
This commit is contained in:
@@ -8,7 +8,11 @@
|
||||
],
|
||||
"env": {
|
||||
"DEFAULT_MINIMUM_TOKENS": ""
|
||||
}
|
||||
},
|
||||
"alwaysAllow": [
|
||||
"resolve-library-id",
|
||||
"get-library-docs"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 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.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
|
@@ -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,
|
||||
);
|
||||
},
|
||||
|
@@ -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<AddSupplementScreen> {
|
||||
.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<AddSupplementScreen> {
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -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<ArchivedSupplementsScreen> {
|
||||
onPressed: () {
|
||||
context.read<SupplementProvider>().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<ArchivedSupplementsScreen> {
|
||||
onPressed: () {
|
||||
context.read<SupplementProvider>().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'),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@@ -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<DebugNotificationsScreen> {
|
||||
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<DebugNotificationsScreen> {
|
||||
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<DebugNotificationsScreen> {
|
||||
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<DebugNotificationsScreen> {
|
||||
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'),
|
||||
|
@@ -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<HistoryScreen> {
|
||||
context.read<SupplementProvider>().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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@@ -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<int>(
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -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<SimpleSyncSettingsScreen> {
|
||||
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<SimpleSyncSettingsScreen> {
|
||||
);
|
||||
|
||||
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<SimpleSyncSettingsScreen> {
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -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<SupplementProvider>().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<SupplementProvider>().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'),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@@ -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<void> 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),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@@ -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<void> 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user