Implement Notifications and Pad Tracking Enhancements

This commit is contained in:
2026-01-08 15:46:28 -06:00
parent 9ae77e7ab0
commit 512577b092
19 changed files with 3059 additions and 1576 deletions

View File

@@ -12,14 +12,14 @@ import '../devotional/devotional_screen.dart';
import '../settings/appearance_screen.dart';
import '../settings/cycle_settings_screen.dart';
import '../settings/relationship_settings_screen.dart';
import '../settings/goal_settings_screen.dart';
import '../settings/goal_settings_screen.dart';
import '../settings/cycle_history_screen.dart';
import '../settings/sharing_settings_screen.dart';
import '../settings/notification_settings_screen.dart';
import '../settings/privacy_settings_screen.dart';
import '../settings/supplies_settings_screen.dart';
import '../settings/export_data_screen.dart';
import '../learn/wife_learn_screen.dart';
import '../learn/wife_learn_screen.dart';
import '../../widgets/tip_card.dart';
import '../../widgets/cycle_ring.dart';
import '../../widgets/scripture_card.dart';
@@ -37,7 +37,8 @@ class HomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedIndex = ref.watch(navigationProvider);
final isPadTrackingEnabled = ref.watch(userProfileProvider.select((u) => u?.isPadTrackingEnabled ?? false));
final isPadTrackingEnabled = ref.watch(
userProfileProvider.select((u) => u?.isPadTrackingEnabled ?? false));
final List<Widget> tabs;
final List<BottomNavigationBarItem> navBarItems;
@@ -50,16 +51,38 @@ class HomeScreen extends ConsumerWidget {
const LogScreen(),
const DevotionalScreen(),
const WifeLearnScreen(),
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
_SettingsTab(
onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
];
navBarItems = [
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'),
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'),
const BottomNavigationBarItem(icon: Icon(Icons.inventory_2_outlined), activeIcon: Icon(Icons.inventory_2), label: 'Supplies'),
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'),
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'),
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'),
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'),
const BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home'),
const BottomNavigationBarItem(
icon: Icon(Icons.calendar_today_outlined),
activeIcon: Icon(Icons.calendar_today),
label: 'Calendar'),
const BottomNavigationBarItem(
icon: Icon(Icons.inventory_2_outlined),
activeIcon: Icon(Icons.inventory_2),
label: 'Supplies'),
const BottomNavigationBarItem(
icon: Icon(Icons.add_circle_outline),
activeIcon: Icon(Icons.add_circle),
label: 'Log'),
const BottomNavigationBarItem(
icon: Icon(Icons.menu_book_outlined),
activeIcon: Icon(Icons.menu_book),
label: 'Devotional'),
const BottomNavigationBarItem(
icon: Icon(Icons.school_outlined),
activeIcon: Icon(Icons.school),
label: 'Learn'),
const BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
label: 'Settings'),
];
} else {
tabs = [
@@ -68,15 +91,34 @@ class HomeScreen extends ConsumerWidget {
const DevotionalScreen(),
const LogScreen(),
const WifeLearnScreen(),
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
_SettingsTab(
onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
];
navBarItems = [
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'),
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'),
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'),
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'),
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'),
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'),
const BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home'),
const BottomNavigationBarItem(
icon: Icon(Icons.calendar_today_outlined),
activeIcon: Icon(Icons.calendar_today),
label: 'Calendar'),
const BottomNavigationBarItem(
icon: Icon(Icons.menu_book_outlined),
activeIcon: Icon(Icons.menu_book),
label: 'Devotional'),
const BottomNavigationBarItem(
icon: Icon(Icons.add_circle_outline),
activeIcon: Icon(Icons.add_circle),
label: 'Log'),
const BottomNavigationBarItem(
icon: Icon(Icons.school_outlined),
activeIcon: Icon(Icons.school),
label: 'Learn'),
const BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
label: 'Settings'),
];
}
@@ -134,7 +176,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
@override
Widget build(BuildContext context) {
// Listen for changes in the cycle info to re-initialize scripture if needed
ref.listen<CycleInfo>(currentCycleInfoProvider, (previousCycleInfo, newCycleInfo) {
ref.listen<CycleInfo>(currentCycleInfoProvider,
(previousCycleInfo, newCycleInfo) {
if (previousCycleInfo?.phase != newCycleInfo.phase) {
_initializeScripture();
}
@@ -145,8 +188,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
final translation =
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation)) ??
BibleTranslation.esv;
final role = ref.watch(userProfileProvider.select((u) => u?.role)) ??
UserRole.wife;
final role =
ref.watch(userProfileProvider.select((u) => u?.role)) ?? UserRole.wife;
final isMarried =
ref.watch(userProfileProvider.select((u) => u?.isMarried)) ?? false;
final averageCycleLength =
@@ -163,7 +206,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
final maxIndex = scriptureState.maxIndex;
if (scripture == null) {
return const Center(child: CircularProgressIndicator()); // Or some error message
return const Center(
child: CircularProgressIndicator()); // Or some error message
}
return SafeArea(
@@ -181,10 +225,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
phase: phase,
),
),
if (phase == CyclePhase.menstrual) ...[
const SizedBox(height: 24),
const PadTrackerCard(),
],
const SizedBox(height: 24),
const PadTrackerCard(),
const SizedBox(height: 32),
// Main Scripture Card with Navigation
Stack(
@@ -203,8 +245,9 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
left: 0,
child: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () =>
ref.read(scriptureProvider.notifier).getPreviousScripture(),
onPressed: () => ref
.read(scriptureProvider.notifier)
.getPreviousScripture(),
color: AppColors.charcoal,
),
),
@@ -212,8 +255,9 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
right: 0,
child: IconButton(
icon: Icon(Icons.arrow_forward_ios),
onPressed: () =>
ref.read(scriptureProvider.notifier).getNextScripture(),
onPressed: () => ref
.read(scriptureProvider.notifier)
.getNextScripture(),
color: AppColors.charcoal,
),
),
@@ -222,16 +266,17 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
),
const SizedBox(height: 16),
if (maxIndex != null && maxIndex > 1)
Center(
child: TextButton.icon(
onPressed: () => ref.read(scriptureProvider.notifier).getRandomScripture(),
icon: const Icon(Icons.shuffle),
label: const Text('Random Verse'),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.primary,
Center(
child: TextButton.icon(
onPressed: () =>
ref.read(scriptureProvider.notifier).getRandomScripture(),
icon: const Icon(Icons.shuffle),
label: const Text('Random Verse'),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.primary,
),
),
),
),
const SizedBox(height: 24),
Text(
'Quick Log',
@@ -243,8 +288,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
const SizedBox(height: 12),
const QuickLogButtons(),
const SizedBox(height: 24),
if (role == UserRole.wife)
_buildWifeTipsSection(context),
if (role == UserRole.wife) _buildWifeTipsSection(context),
const SizedBox(height: 20),
],
),
@@ -364,18 +408,18 @@ class _SettingsTab extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final name =
ref.watch(userProfileProvider.select((u) => u?.name)) ?? 'Guest';
final roleSymbol =
ref.watch(userProfileProvider.select((u) => u?.role)) ==
UserRole.husband
? 'HUSBAND'
: null;
final roleSymbol = ref.watch(userProfileProvider.select((u) => u?.role)) ==
UserRole.husband
? 'HUSBAND'
: null;
final relationshipStatus = ref.watch(userProfileProvider
.select((u) => u?.relationshipStatus.name.toUpperCase())) ??
'SINGLE';
final translationLabel =
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ??
'ESV';
final isSingle = ref.watch(userProfileProvider.select((u) => u?.relationshipStatus == RelationshipStatus.single));
final translationLabel = ref.watch(
userProfileProvider.select((u) => u?.bibleTranslation.label)) ??
'ESV';
final isSingle = ref.watch(userProfileProvider
.select((u) => u?.relationshipStatus == RelationshipStatus.single));
return SafeArea(
child: SingleChildScrollView(
@@ -398,8 +442,10 @@ class _SettingsTab extends ConsumerWidget {
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.05)),
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.05)),
),
child: Row(
children: [
@@ -409,8 +455,14 @@ class _SettingsTab extends ConsumerWidget {
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.7),
Theme.of(context).colorScheme.secondary.withOpacity(0.7)
Theme.of(context)
.colorScheme
.primary
.withOpacity(0.7),
Theme.of(context)
.colorScheme
.secondary
.withOpacity(0.7)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
@@ -459,14 +511,15 @@ class _SettingsTab extends ConsumerWidget {
const SizedBox(height: 24),
_buildSettingsGroup(context, 'Preferences', [
_buildSettingsTile(
context,
Icons.notifications_outlined,
context,
Icons.notifications_outlined,
'Notifications',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NotificationSettingsScreen()));
builder: (context) =>
const NotificationSettingsScreen()));
},
),
_buildSettingsTile(
@@ -477,7 +530,8 @@ class _SettingsTab extends ConsumerWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SuppliesSettingsScreen()));
builder: (context) =>
const SuppliesSettingsScreen()));
},
),
_buildSettingsTile(
@@ -488,12 +542,13 @@ class _SettingsTab extends ConsumerWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RelationshipSettingsScreen()));
builder: (context) =>
const RelationshipSettingsScreen()));
},
),
_buildSettingsTile(
context,
Icons.flag_outlined,
Icons.flag_outlined,
'Cycle Goal',
onTap: () {
Navigator.push(
@@ -521,8 +576,7 @@ class _SettingsTab extends ConsumerWidget {
'My Favorites',
onTap: () => _showFavoritesDialog(context, ref),
),
_buildSettingsTile(
context, Icons.security, 'Privacy & Security',
_buildSettingsTile(context, Icons.security, 'Privacy & Security',
onTap: () {
Navigator.push(
context,
@@ -562,8 +616,7 @@ class _SettingsTab extends ConsumerWidget {
builder: (context) => CycleHistoryScreen()));
}),
_buildSettingsTile(
context, Icons.download_outlined, 'Export Data',
onTap: () {
context, Icons.download_outlined, 'Export Data', onTap: () {
Navigator.push(
context,
MaterialPageRoute(
@@ -629,7 +682,9 @@ class _SettingsTab extends ConsumerWidget {
autofocus: true,
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel')),
ElevatedButton(
onPressed: () => Navigator.pop(context, controller.text),
child: const Text('Unlock'),
@@ -648,7 +703,8 @@ class _SettingsTab extends ConsumerWidget {
final granted = await _authenticate(context, userProfile.privacyPin!);
if (!granted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Incorrect PIN')));
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Incorrect PIN')));
}
return;
}
@@ -663,14 +719,16 @@ class _SettingsTab extends ConsumerWidget {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('My Favorites', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
title: Text('My Favorites',
style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'List your favorite comfort foods, snacks, or flowers so your husband knows what to get you!',
style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray),
style:
GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray),
),
const SizedBox(height: 16),
TextField(
@@ -696,8 +754,11 @@ class _SettingsTab extends ConsumerWidget {
.where((e) => e.isNotEmpty)
.toList();
final updatedProfile = userProfile.copyWith(favoriteFoods: favorites);
ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
final updatedProfile =
userProfile.copyWith(favoriteFoods: favorites);
ref
.read(userProfileProvider.notifier)
.updateProfile(updatedProfile);
Navigator.pop(context);
},
child: const Text('Save'),
@@ -710,14 +771,16 @@ class _SettingsTab extends ConsumerWidget {
void _showShareDialog(BuildContext context, WidgetRef ref) {
// Generate a simple pairing code (in a real app, this would be stored/validated)
final userProfile = ref.read(userProfileProvider);
final pairingCode = userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123';
final pairingCode =
userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123';
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.share_outlined, color: Theme.of(context).colorScheme.primary),
Icon(Icons.share_outlined,
color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 8),
const Text('Share with Husband'),
],
@@ -727,7 +790,8 @@ class _SettingsTab extends ConsumerWidget {
children: [
Text(
'Share this code with your husband so he can connect to your cycle data:',
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
style:
GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
),
const SizedBox(height: 24),
Container(
@@ -735,7 +799,9 @@ class _SettingsTab extends ConsumerWidget {
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)),
border: Border.all(
color:
Theme.of(context).colorScheme.primary.withOpacity(0.3)),
),
child: SelectableText(
pairingCode,
@@ -750,7 +816,8 @@ class _SettingsTab extends ConsumerWidget {
const SizedBox(height: 16),
Text(
'He can enter this in his app under Settings > Connect with Wife.',
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
style:
GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
textAlign: TextAlign.center,
),
],
@@ -868,4 +935,4 @@ Widget _buildTipCard(
),
],
);
}
}