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

@@ -37,7 +37,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
final entries = ref.watch(cycleEntriesProvider);
final user = ref.watch(userProfileProvider);
final isIrregular = user?.isIrregularCycle ?? false;
int cycleLength = user?.averageCycleLength ?? 28;
if (isIrregular) {
if (_predictionMode == PredictionMode.short) {
@@ -46,237 +46,257 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
cycleLength = user?.maxCycleLength ?? 35;
}
}
final lastPeriodStart = user?.lastPeriodStartDate;
return ProtectedContentWrapper(
title: 'Calendar',
isProtected: user?.isCalendarProtected ?? false,
userProfile: user,
child: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Row(
title: 'Calendar',
isProtected: user?.isCalendarProtected ?? false,
userProfile: user,
child: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Expanded(
child: Text(
'Calendar',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
),
_buildLegendButton(),
],
),
if (isIrregular) ...[
const SizedBox(height: 16),
_buildPredictionToggle(),
],
],
),
),
// Calendar
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: TableCalendar(
firstDay: DateTime.now().subtract(const Duration(days: 365)),
lastDay: DateTime.now().add(const Duration(days: 365)),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onFormatChanged: (format) {
setState(() => _calendarFormat = format);
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
},
calendarStyle: CalendarStyle(
outsideDaysVisible: false,
defaultTextStyle: GoogleFonts.outfit(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.charcoal,
),
weekendTextStyle: GoogleFonts.outfit(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.charcoal,
),
todayDecoration: BoxDecoration(
color: AppColors.sageGreen.withOpacity(0.3),
shape: BoxShape.circle,
),
todayTextStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.sageGreen,
),
selectedDecoration: const BoxDecoration(
color: AppColors.sageGreen,
shape: BoxShape.circle,
),
selectedTextStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
titleTextStyle: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.titleLarge?.color ?? AppColors.charcoal,
),
leftChevronIcon: Icon(
Icons.chevron_left,
color: Theme.of(context).iconTheme.color ?? AppColors.warmGray,
),
rightChevronIcon: Icon(
Icons.chevron_right,
color: Theme.of(context).iconTheme.color ?? AppColors.warmGray,
),
),
daysOfWeekStyle: DaysOfWeekStyle(
weekdayStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodySmall?.color ?? AppColors.warmGray,
),
weekendStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodySmall?.color ?? AppColors.warmGray,
),
),
calendarBuilders: CalendarBuilders(
defaultBuilder: (context, day, focusedDay) {
return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isSelected: false, isToday: false);
},
todayBuilder: (context, day, focusedDay) {
return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isToday: true);
},
selectedBuilder: (context, day, focusedDay) {
return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isSelected: true);
},
markerBuilder: (context, date, events) {
final entry = _getEntryForDate(date, entries);
if (entry == null) {
final phase =
_getPhaseForDate(date, lastPeriodStart, cycleLength);
if (phase != null) {
return Positioned(
bottom: 4,
child: Container(
width: 5,
height: 5,
decoration: BoxDecoration(
color: _getPhaseColor(phase),
shape: BoxShape.circle,
),
),
);
}
return null;
}
// If we have an entry, show icons/markers
return Positioned(
bottom: 4,
child: Row(
mainAxisSize: MainAxisSize.min,
Row(
children: [
if (entry.isPeriodDay)
Container(
width: 6,
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: const BoxDecoration(
color: AppColors.menstrualPhase,
shape: BoxShape.circle,
),
),
if (entry.mood != null ||
entry.energyLevel != 3 ||
entry.hasSymptoms)
Container(
width: 6,
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: const BoxDecoration(
color: AppColors.softGold,
shape: BoxShape.circle,
Expanded(
child: Text(
'Calendar',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.textTheme
.headlineLarge
?.color,
),
),
),
_buildLegendButton(),
],
),
);
},
if (isIrregular) ...[
const SizedBox(height: 16),
_buildPredictionToggle(),
],
],
),
),
),
),
const SizedBox(height: 24),
// Divider / Header for Day Info
if (_selectedDay != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Text(
'Daily Log',
style: GoogleFonts.outfit(
fontSize: 16,
// Calendar
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: TableCalendar(
firstDay:
DateTime.now().subtract(const Duration(days: 365)),
lastDay: DateTime.now().add(const Duration(days: 365)),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onFormatChanged: (format) {
setState(() => _calendarFormat = format);
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
},
calendarStyle: CalendarStyle(
outsideDaysVisible: false,
defaultTextStyle: GoogleFonts.outfit(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color ??
AppColors.charcoal,
),
weekendTextStyle: GoogleFonts.outfit(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color ??
AppColors.charcoal,
),
todayDecoration: BoxDecoration(
color: AppColors.sageGreen.withOpacity(0.3),
shape: BoxShape.circle,
),
todayTextStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.warmGray,
letterSpacing: 1,
color: AppColors.sageGreen,
),
selectedDecoration: const BoxDecoration(
color: AppColors.sageGreen,
shape: BoxShape.circle,
),
selectedTextStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(width: 12),
const Expanded(child: Divider(color: AppColors.lightGray)),
],
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
titleTextStyle: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.titleLarge?.color ??
AppColors.charcoal,
),
leftChevronIcon: Icon(
Icons.chevron_left,
color: Theme.of(context).iconTheme.color ??
AppColors.warmGray,
),
rightChevronIcon: Icon(
Icons.chevron_right,
color: Theme.of(context).iconTheme.color ??
AppColors.warmGray,
),
),
daysOfWeekStyle: DaysOfWeekStyle(
weekdayStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodySmall?.color ??
AppColors.warmGray,
),
weekendStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodySmall?.color ??
AppColors.warmGray,
),
),
calendarBuilders: CalendarBuilders(
defaultBuilder: (context, day, focusedDay) {
return _buildCalendarDay(day, focusedDay, entries,
lastPeriodStart, cycleLength,
isSelected: false, isToday: false);
},
todayBuilder: (context, day, focusedDay) {
return _buildCalendarDay(day, focusedDay, entries,
lastPeriodStart, cycleLength,
isToday: true);
},
selectedBuilder: (context, day, focusedDay) {
return _buildCalendarDay(day, focusedDay, entries,
lastPeriodStart, cycleLength,
isSelected: true);
},
markerBuilder: (context, date, events) {
final entry = _getEntryForDate(date, entries);
if (entry == null) {
final phase = _getPhaseForDate(
date, lastPeriodStart, cycleLength);
if (phase != null) {
return Positioned(
bottom: 4,
child: Container(
width: 5,
height: 5,
decoration: BoxDecoration(
color: _getPhaseColor(phase),
shape: BoxShape.circle,
),
),
);
}
return null;
}
// If we have an entry, show icons/markers
return Positioned(
bottom: 4,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (entry.isPeriodDay)
Container(
width: 6,
height: 6,
margin:
const EdgeInsets.symmetric(horizontal: 1),
decoration: const BoxDecoration(
color: AppColors.menstrualPhase,
shape: BoxShape.circle,
),
),
if (entry.mood != null ||
entry.energyLevel != 3 ||
entry.hasSymptoms)
Container(
width: 6,
height: 6,
margin:
const EdgeInsets.symmetric(horizontal: 1),
decoration: const BoxDecoration(
color: AppColors.softGold,
shape: BoxShape.circle,
),
),
],
),
);
},
),
),
),
),
const SizedBox(height: 12),
// Day Info (No longer Expanded)
_buildDayInfo(
_selectedDay!, lastPeriodStart, cycleLength, entries, user),
const SizedBox(height: 40), // Bottom padding
],
],
),
),
));
const SizedBox(height: 24),
// Divider / Header for Day Info
if (_selectedDay != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Text(
'Daily Log',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.warmGray,
letterSpacing: 1,
),
),
const SizedBox(width: 12),
const Expanded(
child: Divider(color: AppColors.lightGray)),
],
),
),
const SizedBox(height: 12),
// Day Info (No longer Expanded)
_buildDayInfo(_selectedDay!, lastPeriodStart, cycleLength,
entries, user),
const SizedBox(height: 40), // Bottom padding
],
],
),
),
));
}
Widget _buildPredictionToggle() {
@@ -288,9 +308,12 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
),
child: Row(
children: [
_buildToggleItem(PredictionMode.short, 'Short (-)', AppColors.menstrualPhase),
_buildToggleItem(PredictionMode.regular, 'Regular', AppColors.sageGreen),
_buildToggleItem(PredictionMode.long, 'Long (+)', AppColors.lutealPhase),
_buildToggleItem(
PredictionMode.short, 'Short (-)', AppColors.menstrualPhase),
_buildToggleItem(
PredictionMode.regular, 'Regular', AppColors.sageGreen),
_buildToggleItem(
PredictionMode.long, 'Long (+)', AppColors.lutealPhase),
],
),
);
@@ -415,8 +438,8 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
);
}
Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, int cycleLength,
List<CycleEntry> entries, UserProfile? user) {
Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart,
int cycleLength, List<CycleEntry> entries, UserProfile? user) {
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
final entry = _getEntryForDate(date, entries);
@@ -487,17 +510,18 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
.bodyMedium
?.copyWith(color: AppColors.warmGray),
),
if (user?.isPadTrackingEnabled == true &&
phase != CyclePhase.menstrual &&
(user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ?? false)) ...[
if (user?.isPadTrackingEnabled == true &&
phase != CyclePhase.menstrual &&
(user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ??
false)) ...[
const SizedBox(height: 16),
_buildPantylinerPrompt(date, null),
],
] else ...[
// Period Detail
if (entry.isPeriodDay)
_buildDetailRow(Icons.water_drop, 'Period Day',
AppColors.menstrualPhase,
_buildDetailRow(
Icons.water_drop, 'Period Day', AppColors.menstrualPhase,
value: entry.flowIntensity?.label),
// Mood Detail
@@ -524,11 +548,11 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
if (user?.isPadTrackingEnabled == true) ...[
const SizedBox(height: 16),
if (entry.usedPantyliner)
_buildDetailRow(Icons.layers_outlined, 'Supplies Used', AppColors.menstrualPhase,
_buildDetailRow(Icons.layers_outlined, 'Supplies Used',
AppColors.menstrualPhase,
value: '${entry.pantylinerCount}'),
if (!entry.usedPantyliner && !entry.isPeriodDay)
_buildPantylinerPrompt(date, entry),
_buildPantylinerPrompt(date, entry),
],
// Notes
@@ -544,16 +568,15 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
fontWeight: FontWeight.w600,
color: AppColors.warmGray)),
const SizedBox(height: 4),
Text(entry.notes!,
style: GoogleFonts.outfit(fontSize: 14)),
Text(entry.notes!, style: GoogleFonts.outfit(fontSize: 14)),
],
),
),
],
if (user?.isPadTrackingEnabled == true) ...[
const SizedBox(height: 16),
_buildManualSupplyEntryButton(date),
const SizedBox(height: 16),
_buildManualSupplyEntryButton(date),
],
const SizedBox(height: 24),
@@ -581,17 +604,17 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
? Icons.edit_note
: Icons.add_circle_outline),
label: Text(entry != null ? 'Edit Log' : 'Add Log'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
),
),
),
],
),
],
),
],
),
);
@@ -608,33 +631,38 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
),
child: Row(
children: [
const Icon(Icons.help_outline, color: AppColors.menstrualPhase, size: 20),
const Icon(Icons.help_outline,
color: AppColors.menstrualPhase, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
'Did you use pantyliners today?',
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal),
style:
GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal),
),
),
TextButton(
onPressed: () {
if (entry != null) {
ref.read(cycleEntriesProvider.notifier).updateEntry(
entry.copyWith(usedPantyliner: true, pantylinerCount: 1),
);
} else {
final newEntry = CycleEntry(
id: const Uuid().v4(),
date: date,
usedPantyliner: true,
pantylinerCount: 1,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
ref.read(cycleEntriesProvider.notifier).addEntry(newEntry);
}
if (entry != null) {
ref.read(cycleEntriesProvider.notifier).updateEntry(
entry.copyWith(usedPantyliner: true, pantylinerCount: 1),
);
} else {
final newEntry = CycleEntry(
id: const Uuid().v4(),
date: date,
usedPantyliner: true,
pantylinerCount: 1,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
ref.read(cycleEntriesProvider.notifier).addEntry(newEntry);
}
},
child: Text('Yes', style: GoogleFonts.outfit(color: AppColors.menstrualPhase, fontWeight: FontWeight.bold)),
child: Text('Yes',
style: GoogleFonts.outfit(
color: AppColors.menstrualPhase,
fontWeight: FontWeight.bold)),
),
],
),
@@ -656,7 +684,8 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.menstrualPhase,
side: const BorderSide(color: AppColors.menstrualPhase),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
);
@@ -832,16 +861,12 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
return entry?.isPeriodDay ?? false;
}
Widget _buildCalendarDay(
DateTime day,
DateTime focusedDay,
List<CycleEntry> entries,
DateTime? lastPeriodStart,
int cycleLength,
Widget _buildCalendarDay(DateTime day, DateTime focusedDay,
List<CycleEntry> entries, DateTime? lastPeriodStart, int cycleLength,
{bool isSelected = false, bool isToday = false, bool isWeekend = false}) {
final phase = _getPhaseForDate(day, lastPeriodStart, cycleLength);
final isDark = Theme.of(context).brightness == Brightness.dark;
// Determine the Day of Cycle
int? doc;
if (lastPeriodStart != null) {
@@ -876,14 +901,22 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
// Text style
TextStyle textStyle = GoogleFonts.outfit(
fontSize: (isOvulationDay || isPeriodStart) ? 18 : 14,
fontWeight: (isOvulationDay || isPeriodStart) ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.white : (isToday ? AppColors.sageGreen : (Theme.of(context).textTheme.bodyMedium?.color)),
fontWeight: (isOvulationDay || isPeriodStart)
? FontWeight.bold
: FontWeight.normal,
color: isSelected
? Colors.white
: (isToday
? AppColors.sageGreen
: (Theme.of(context).textTheme.bodyMedium?.color)),
);
if (isOvulationDay) {
textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.ovulationPhase);
textStyle = textStyle.copyWith(
color: isSelected ? Colors.white : AppColors.ovulationPhase);
} else if (isPeriodStart) {
textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.menstrualPhase);
textStyle = textStyle.copyWith(
color: isSelected ? Colors.white : AppColors.menstrualPhase);
}
return Container(

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(
),
],
);
}
}

View File

@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/user_profile.dart';
import '../../providers/user_provider.dart';
import '../../theme/app_theme.dart';
/// Dedicated Appearance Settings for the Husband App
/// These settings only affect the husband's experience
class HusbandAppearanceScreen extends ConsumerWidget {
const HusbandAppearanceScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userProfile = ref.watch(userProfileProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: Text(
'Appearance',
style: Theme.of(context).appBarTheme.titleTextStyle,
),
centerTitle: true,
),
body: userProfile == null
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(16.0),
children: [
_buildThemeModeSelector(
context, ref, userProfile.husbandThemeMode, isDark),
const SizedBox(height: 24),
_buildAccentColorSelector(
context, ref, userProfile.husbandAccentColor, isDark),
],
),
);
}
Widget _buildThemeModeSelector(BuildContext context, WidgetRef ref,
AppThemeMode currentMode, bool isDark) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Theme Mode',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
SegmentedButton<AppThemeMode>(
segments: const [
ButtonSegment(
value: AppThemeMode.light,
label: Text('Light'),
icon: Icon(Icons.light_mode),
),
ButtonSegment(
value: AppThemeMode.dark,
label: Text('Dark'),
icon: Icon(Icons.dark_mode),
),
ButtonSegment(
value: AppThemeMode.system,
label: Text('System'),
icon: Icon(Icons.brightness_auto),
),
],
selected: {currentMode},
onSelectionChanged: (Set<AppThemeMode> newSelection) async {
if (newSelection.isNotEmpty) {
final profile = ref.read(userProfileProvider);
if (profile != null) {
await ref.read(userProfileProvider.notifier).updateProfile(
profile.copyWith(husbandThemeMode: newSelection.first),
);
}
}
},
),
],
);
}
Widget _buildAccentColorSelector(
BuildContext context, WidgetRef ref, String currentAccent, bool isDark) {
// Navy/blue themed colors for husband app
final accents = [
{'color': AppColors.navyBlue, 'value': '0xFF1A3A5C'},
{'color': AppColors.steelBlue, 'value': '0xFF5C7892'},
{'color': AppColors.sageGreen, 'value': '0xFFA8C5A8'},
{'color': AppColors.info, 'value': '0xFF7BB8E8'},
{'color': AppColors.teal, 'value': '0xFF5B9AA0'},
{'color': const Color(0xFF6B5B95), 'value': '0xFF6B5B95'}, // Purple
{'color': const Color(0xFF3D5A80), 'value': '0xFF3D5A80'}, // Dark Blue
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Accent Color',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Wrap(
spacing: 16,
runSpacing: 16,
children: accents.map((accent) {
final color = accent['color'] as Color;
final value = accent['value'] as String;
final isSelected = currentAccent == value;
return GestureDetector(
onTap: () async {
final profile = ref.read(userProfileProvider);
if (profile != null) {
await ref.read(userProfileProvider.notifier).updateProfile(
profile.copyWith(husbandAccentColor: value),
);
}
},
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isSelected
? Border.all(
color: isDark ? Colors.white : AppColors.charcoal,
width: 3,
)
: Border.all(
color: isDark ? Colors.white30 : Colors.black12,
width: 1,
),
boxShadow: [
if (isSelected)
BoxShadow(
color: color.withOpacity(0.4),
blurRadius: 8,
offset: const Offset(0, 4),
)
],
),
child: isSelected
? const Icon(Icons.check, color: Colors.white)
: null,
),
);
}).toList(),
),
],
);
}
}

View File

@@ -2,20 +2,23 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import '../../models/user_profile.dart';
import '../../models/user_profile.dart';
import '../../models/teaching_plan.dart';
import '../../providers/user_provider.dart';
import '../../theme/app_theme.dart';
import '../../services/bible_xml_parser.dart';
import '../../services/mock_data_service.dart';
class HusbandDevotionalScreen extends ConsumerStatefulWidget {
const HusbandDevotionalScreen({super.key});
@override
ConsumerState<HusbandDevotionalScreen> createState() => _HusbandDevotionalScreenState();
ConsumerState<HusbandDevotionalScreen> createState() =>
_HusbandDevotionalScreenState();
}
class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScreen> {
class _HusbandDevotionalScreenState
extends ConsumerState<HusbandDevotionalScreen> {
final _parser = BibleXmlParser();
Map<String, String> _scriptures = {};
bool _loading = true;
@@ -34,15 +37,16 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
Future<void> _fetchScriptures() async {
final user = ref.read(userProfileProvider);
if (user == null) return;
final translation = user.bibleTranslation;
if (translation == _currentTranslation && _scriptures.isNotEmpty) return;
setState(() => _loading = true);
try {
final assetPath = 'assets/bible_xml/${translation.name.toUpperCase()}.xml';
final assetPath =
'assets/bible_xml/${translation.name.toUpperCase()}.xml';
// Define verses to fetch
final versesToFetch = [
'1 Corinthians 11:3',
@@ -53,7 +57,7 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
];
final Map<String, String> results = {};
for (final ref in versesToFetch) {
final text = await _parser.getVerseFromAsset(assetPath, ref);
results[ref] = text ?? 'Verse not found.';
@@ -74,7 +78,8 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
void _showAddTeachingDialog([TeachingPlan? existingPlan]) {
final titleController = TextEditingController(text: existingPlan?.topic);
final scriptureController = TextEditingController(text: existingPlan?.scriptureReference);
final scriptureController =
TextEditingController(text: existingPlan?.scriptureReference);
final notesController = TextEditingController(text: existingPlan?.notes);
DateTime selectedDate = existingPlan?.date ?? DateTime.now();
@@ -125,7 +130,8 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
context: context,
initialDate: selectedDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
lastDate:
DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
setState(() => selectedDate = picked);
@@ -145,41 +151,50 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
),
ElevatedButton(
onPressed: () async {
if (titleController.text.isEmpty) return;
if (titleController.text.isEmpty) return;
final user = ref.read(userProfileProvider);
if (user == null) return;
final user = ref.read(userProfileProvider);
if (user == null) return;
TeachingPlan newPlan;
if (existingPlan != null) {
newPlan = existingPlan.copyWith(
topic: titleController.text,
scriptureReference: scriptureController.text,
notes: notesController.text,
date: selectedDate,
);
} else {
newPlan = TeachingPlan.create(
topic: titleController.text,
scriptureReference: scriptureController.text,
notes: notesController.text,
date: selectedDate,
);
}
TeachingPlan newPlan;
if (existingPlan != null) {
newPlan = existingPlan.copyWith(
topic: titleController.text,
scriptureReference: scriptureController.text,
notes: notesController.text,
date: selectedDate,
);
} else {
newPlan = TeachingPlan.create(
topic: titleController.text,
scriptureReference: scriptureController.text,
notes: notesController.text,
date: selectedDate,
);
}
List<TeachingPlan> updatedList = List.from(user.teachingPlans ?? []);
if (existingPlan != null) {
final index = updatedList.indexWhere((p) => p.id == existingPlan.id);
if (index != -1) updatedList[index] = newPlan;
} else {
updatedList.add(newPlan);
}
List<TeachingPlan> updatedList =
List.from(user.teachingPlans ?? []);
if (existingPlan != null) {
final index =
updatedList.indexWhere((p) => p.id == existingPlan.id);
if (index != -1) updatedList[index] = newPlan;
} else {
updatedList.add(newPlan);
}
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList),
);
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList),
);
if (mounted) Navigator.pop(context);
// Trigger notification for new teaching plans
if (existingPlan == null) {
NotificationService().showTeachingPlanNotification(
teacherName: user.name ?? 'Husband',
);
}
if (mounted) Navigator.pop(context);
},
child: const Text('Save'),
),
@@ -193,10 +208,11 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
final user = ref.read(userProfileProvider);
if (user == null || user.teachingPlans == null) return;
final updatedList = user.teachingPlans!.where((p) => p.id != plan.id).toList();
final updatedList =
user.teachingPlans!.where((p) => p.id != plan.id).toList();
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList),
);
user.copyWith(teachingPlans: updatedList),
);
}
void _toggleComplete(TeachingPlan plan) async {
@@ -207,17 +223,17 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
if (p.id == plan.id) return p.copyWith(isCompleted: !p.isCompleted);
return p;
}).toList();
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList),
);
user.copyWith(teachingPlans: updatedList),
);
}
@override
Widget build(BuildContext context) {
final user = ref.watch(userProfileProvider);
final upcomingPlans = user?.teachingPlans ?? [];
upcomingPlans.sort((a,b) => a.date.compareTo(b.date));
upcomingPlans.sort((a, b) => a.date.compareTo(b.date));
// Listen for translation changes to re-fetch
ref.listen(userProfileProvider, (prev, next) {
@@ -253,12 +269,13 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
),
IconButton(
onPressed: () => _showAddTeachingDialog(),
icon: const Icon(Icons.add_circle, color: AppColors.navyBlue, size: 28),
icon: const Icon(Icons.add_circle,
color: AppColors.navyBlue, size: 28),
),
],
),
const SizedBox(height: 12),
if (upcomingPlans.isEmpty)
Container(
width: double.infinity,
@@ -303,39 +320,48 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
onDismissed: (_) => _deletePlan(plan),
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
child: ListTile(
onTap: () => _showAddTeachingDialog(plan),
leading: IconButton(
icon: Icon(
plan.isCompleted ? Icons.check_circle : Icons.circle_outlined,
color: plan.isCompleted ? Colors.green : Colors.grey
),
onPressed: () => _toggleComplete(plan),
icon: Icon(
plan.isCompleted
? Icons.check_circle
: Icons.circle_outlined,
color: plan.isCompleted
? Colors.green
: Colors.grey),
onPressed: () => _toggleComplete(plan),
),
title: Text(
plan.topic,
style: GoogleFonts.outfit(
fontWeight: FontWeight.w600,
decoration: plan.isCompleted ? TextDecoration.lineThrough : null,
decoration: plan.isCompleted
? TextDecoration.lineThrough
: null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (plan.scriptureReference.isNotEmpty)
Text(plan.scriptureReference, style: const TextStyle(fontWeight: FontWeight.w500)),
Text(plan.scriptureReference,
style: const TextStyle(
fontWeight: FontWeight.w500)),
if (plan.notes.isNotEmpty)
Text(
plan.notes,
maxLines: 2,
plan.notes,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
DateFormat.yMMMd().format(plan.date),
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
const SizedBox(height: 4),
Text(
DateFormat.yMMMd().format(plan.date),
style: TextStyle(
fontSize: 11, color: Colors.grey[600]),
),
],
),
isThreeLine: true,
@@ -344,8 +370,13 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
);
},
),
const SizedBox(height: 40),
const SizedBox(height: 24),
// Prayer Request Section
_buildPrayerRequestSection(context, ref, user),
const SizedBox(height: 40),
],
),
),
@@ -356,11 +387,12 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
// Combine 1 Timothy verses
String timothyText = 'Loading...';
if (!_loading) {
timothyText = '${_scriptures['1 Timothy 3:4'] ?? '...'} ${_scriptures['1 Timothy 3:5'] ?? ''} ... ${_scriptures['1 Timothy 3:12'] ?? ''}';
// Cleanup potential double spaces or missing
timothyText = timothyText.replaceAll(' ', ' ').trim();
timothyText =
'${_scriptures['1 Timothy 3:4'] ?? '...'} ${_scriptures['1 Timothy 3:5'] ?? ''} ... ${_scriptures['1 Timothy 3:12'] ?? ''}';
// Cleanup potential double spaces or missing
timothyText = timothyText.replaceAll(' ', ' ').trim();
}
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@@ -376,28 +408,31 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
const Icon(Icons.menu_book, color: Color(0xFF8B5E3C)),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Biblical Principles',
style: GoogleFonts.lora(
fontSize: 18,
fontWeight: FontWeight.bold,
color: const Color(0xFF5D4037),
),
),
Text(
version,
style: GoogleFonts.outfit(fontSize: 12, color: const Color(0xFF8B5E3C)),
),
],
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Biblical Principles',
style: GoogleFonts.lora(
fontSize: 18,
fontWeight: FontWeight.bold,
color: const Color(0xFF5D4037),
),
),
Text(
version,
style: GoogleFonts.outfit(
fontSize: 12, color: const Color(0xFF8B5E3C)),
),
],
),
],
),
const SizedBox(height: 16),
_buildVerseText(
'1 Corinthians 11:3',
_loading ? 'Loading...' : (_scriptures['1 Corinthians 11:3'] ?? 'Verse not found.'),
_loading
? 'Loading...'
: (_scriptures['1 Corinthians 11:3'] ?? 'Verse not found.'),
'Supports family structure under Christs authority.',
),
const SizedBox(height: 16),
@@ -409,9 +444,11 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
'Qualifications for church elders include managing their own households well.',
),
const SizedBox(height: 16),
_buildVerseText(
_buildVerseText(
'Titus 1:6',
_loading ? 'Loading...' : (_scriptures['Titus 1:6'] ?? 'Verse not found.'),
_loading
? 'Loading...'
: (_scriptures['Titus 1:6'] ?? 'Verse not found.'),
'Husbands who lead faithfully at home are seen as candidates for formal spiritual leadership.',
),
],
@@ -433,17 +470,17 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
),
const SizedBox(height: 4),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
duration: const Duration(milliseconds: 300),
child: Text(
text,
key: ValueKey(text), // Animate change
style: GoogleFonts.lora(
fontSize: 15,
fontStyle: FontStyle.italic,
height: 1.4,
color: const Color(0xFF3E2723),
),
fontSize: 15,
fontStyle: FontStyle.italic,
height: 1.4,
color: const Color(0xFF3E2723),
),
),
),
const SizedBox(height: 4),
Text(
@@ -456,4 +493,252 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
],
);
}
Widget _buildPrayerRequestSection(
BuildContext context, WidgetRef ref, UserProfile? user) {
// Check if connected (partnerName is set)
final isConnected =
user?.partnerName != null && (user?.partnerName?.isNotEmpty ?? false);
// Get today's cycle entry to check for prayer requests
final entries = ref.watch(cycleEntriesProvider);
final todayEntry = entries.isNotEmpty
? entries.firstWhere(
(e) => DateUtils.isSameDay(e.date, DateTime.now()),
orElse: () => entries.first,
)
: null;
final prayerRequest = todayEntry?.prayerRequest;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.lavender.withOpacity(0.15),
AppColors.blushPink.withOpacity(0.15),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.lavender.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('🙏', style: TextStyle(fontSize: 20)),
const SizedBox(width: 8),
Expanded(
child: Text(
'Wife\'s Prayer Requests',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
),
],
),
const SizedBox(height: 16),
if (!isConnected) ...[
Text(
'Connect with your wife to see her prayer requests and pray for her.',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 16),
Center(
child: ElevatedButton.icon(
onPressed: () => _showConnectDialog(context, ref),
icon: const Icon(Icons.link),
label: const Text('Connect with Wife'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
),
] else if (prayerRequest != null && prayerRequest.isNotEmpty) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${user?.partnerName ?? "Wife"} shared:',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
const SizedBox(height: 8),
Text(
prayerRequest,
style: GoogleFonts.lora(
fontSize: 15,
fontStyle: FontStyle.italic,
height: 1.5,
color: AppColors.charcoal,
),
),
],
),
),
const SizedBox(height: 12),
Center(
child: TextButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Praying for her! 🙏'),
backgroundColor: AppColors.sageGreen,
),
);
},
icon: const Icon(Icons.favorite, size: 18),
label: const Text('I\'m Praying'),
style: TextButton.styleFrom(
foregroundColor: AppColors.rose,
),
),
),
] else ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(Icons.favorite_border,
color: AppColors.warmGray, size: 32),
const SizedBox(height: 8),
Text(
'No prayer requests today',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
Text(
'Check back later or encourage her to share.',
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray.withOpacity(0.8),
),
),
],
),
),
],
],
),
);
}
void _showConnectDialog(BuildContext context, WidgetRef ref) {
final codeController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: const [
Icon(Icons.link, color: AppColors.navyBlue),
SizedBox(width: 8),
Text('Connect with Wife'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Enter the pairing code from your wife\'s app:',
style:
GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
),
const SizedBox(height: 16),
TextField(
controller: codeController,
decoration: const InputDecoration(
hintText: 'e.g., ABC123',
border: OutlineInputBorder(),
),
textCapitalization: TextCapitalization.characters,
),
const SizedBox(height: 16),
Text(
'Your wife can find this code in her Devotional screen under "Share with Husband".',
style:
GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
final code = codeController.text.trim();
Navigator.pop(context);
if (code.isNotEmpty) {
// Simulate connection with mock data
final mockService = MockDataService();
final entries = mockService.generateMockCycleEntries();
for (var entry in entries) {
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
}
final mockWife = mockService.generateMockWifeProfile();
final currentProfile = ref.read(userProfileProvider);
if (currentProfile != null) {
final updatedProfile = currentProfile.copyWith(
partnerName: mockWife.name,
averageCycleLength: mockWife.averageCycleLength,
averagePeriodLength: mockWife.averagePeriodLength,
lastPeriodStartDate: mockWife.lastPeriodStartDate,
);
await ref
.read(userProfileProvider.notifier)
.updateProfile(updatedProfile);
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connected with wife! 💑'),
backgroundColor: AppColors.sageGreen,
),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
),
child: const Text('Connect'),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,7 @@ import '../../theme/app_theme.dart';
import '../../models/user_profile.dart';
import '../../providers/user_provider.dart';
import '../../services/mock_data_service.dart';
import '../../providers/cycle_provider.dart'; // Ensure cycleEntriesProvider is available
import '../settings/appearance_screen.dart';
import 'husband_appearance_screen.dart';
class HusbandSettingsScreen extends ConsumerWidget {
const HusbandSettingsScreen({super.key});
@@ -72,20 +71,22 @@ class HusbandSettingsScreen extends ConsumerWidget {
final mockWife = mockService.generateMockWifeProfile();
final currentProfile = ref.read(userProfileProvider);
if (currentProfile != null) {
final updatedProfile = currentProfile.copyWith(
partnerName: mockWife.name,
averageCycleLength: mockWife.averageCycleLength,
averagePeriodLength: mockWife.averagePeriodLength,
lastPeriodStartDate: mockWife.lastPeriodStartDate,
favoriteFoods: mockWife.favoriteFoods,
);
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
final updatedProfile = currentProfile.copyWith(
partnerName: mockWife.name,
averageCycleLength: mockWife.averageCycleLength,
averagePeriodLength: mockWife.averagePeriodLength,
lastPeriodStartDate: mockWife.lastPeriodStartDate,
favoriteFoods: mockWife.favoriteFoods,
);
await ref
.read(userProfileProvider.notifier)
.updateProfile(updatedProfile);
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Demo data loaded')),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Demo data loaded')),
);
}
}
}
@@ -112,23 +113,26 @@ class HusbandSettingsScreen extends ConsumerWidget {
),
const SizedBox(height: 16),
...BibleTranslation.values.map((translation) => ListTile(
title: Text(
translation.label,
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
),
trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation
? const Icon(Icons.check, color: AppColors.sageGreen)
: null,
onTap: () async {
final profile = ref.read(userProfileProvider);
if (profile != null) {
await ref.read(userProfileProvider.notifier).updateProfile(
profile.copyWith(bibleTranslation: translation),
);
}
if (context.mounted) Navigator.pop(context);
},
)),
title: Text(
translation.label,
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
),
trailing: ref.watch(userProfileProvider)?.bibleTranslation ==
translation
? const Icon(Icons.check, color: AppColors.sageGreen)
: null,
onTap: () async {
final profile = ref.read(userProfileProvider);
if (profile != null) {
await ref
.read(userProfileProvider.notifier)
.updateProfile(
profile.copyWith(bibleTranslation: translation),
);
}
if (context.mounted) Navigator.pop(context);
},
)),
],
),
),
@@ -138,104 +142,112 @@ class HusbandSettingsScreen extends ConsumerWidget {
void _showConnectDialog(BuildContext context, WidgetRef ref) {
final codeController = TextEditingController();
bool shareDevotional = true;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: Row(
children: [
const Icon(Icons.link, color: AppColors.navyBlue),
const SizedBox(width: 8),
const Text('Connect with Wife'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Enter the pairing code from your wife\'s app:',
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
),
const SizedBox(height: 16),
TextField(
controller: codeController,
decoration: const InputDecoration(
hintText: 'e.g., ABC123',
border: OutlineInputBorder(),
title: Row(
children: [
const Icon(Icons.link, color: AppColors.navyBlue),
const SizedBox(width: 8),
const Text('Connect with Wife'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Enter the pairing code from your wife\'s app:',
style:
GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
),
textCapitalization: TextCapitalization.characters,
),
const SizedBox(height: 16),
Text(
'Your wife can find this code in her Settings under "Share with Husband".',
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
),
const SizedBox(height: 24),
Row(
const SizedBox(height: 16),
TextField(
controller: codeController,
decoration: const InputDecoration(
hintText: 'e.g., ABC123',
border: OutlineInputBorder(),
),
textCapitalization: TextCapitalization.characters,
),
const SizedBox(height: 16),
Text(
'Your wife can find this code in her Settings under "Share with Husband".',
style:
GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
),
const SizedBox(height: 24),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 24,
width: 24,
child: Checkbox(
value: shareDevotional,
onChanged: (val) => setState(() => shareDevotional = val ?? true),
activeColor: AppColors.navyBlue,
),
SizedBox(
height: 24,
width: 24,
child: Checkbox(
value: shareDevotional,
onChanged: (val) =>
setState(() => shareDevotional = val ?? true),
activeColor: AppColors.navyBlue,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Share Devotional Plans',
style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.charcoal),
),
Text(
'Allow her to see the teaching plans you create.',
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
),
],
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Share Devotional Plans',
style: GoogleFonts.outfit(
fontWeight: FontWeight.bold,
fontSize: 14,
color: AppColors.charcoal),
),
Text(
'Allow her to see the teaching plans you create.',
style: GoogleFonts.outfit(
fontSize: 12, color: AppColors.warmGray),
),
],
),
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
],
),
ElevatedButton(
onPressed: () async {
final code = codeController.text.trim();
Navigator.pop(context);
// Update preference
final user = ref.read(userProfileProvider);
if (user != null) {
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(isDataShared: shareDevotional)
);
}
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
final code = codeController.text.trim();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Settings updated & Connected!'),
backgroundColor: AppColors.sageGreen,
),
);
if (code.isNotEmpty) {
Navigator.pop(context);
// Update preference
final user = ref.read(userProfileProvider);
if (user != null) {
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(isDataShared: shareDevotional));
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Settings updated & Connected!'),
backgroundColor: AppColors.sageGreen,
),
);
if (code.isNotEmpty) {
// Load demo data as simulation
final mockService = MockDataService();
final entries = mockService.generateMockCycleEntries();
for (var entry in entries) {
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
await ref
.read(cycleEntriesProvider.notifier)
.addEntry(entry);
}
final mockWife = mockService.generateMockWifeProfile();
final currentProfile = ref.read(userProfileProvider);
@@ -248,18 +260,20 @@ class HusbandSettingsScreen extends ConsumerWidget {
lastPeriodStartDate: mockWife.lastPeriodStartDate,
favoriteFoods: mockWife.favoriteFoods,
);
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
await ref
.read(userProfileProvider.notifier)
.updateProfile(updatedProfile);
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
),
child: const Text('Connect'),
),
child: const Text('Connect'),
),
],
),
],
),
),
);
}
@@ -268,7 +282,8 @@ class HusbandSettingsScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// Theme aware colors
final isDark = Theme.of(context).brightness == Brightness.dark;
final cardColor = Theme.of(context).cardTheme.color; // Using theme card color
final cardColor =
Theme.of(context).cardTheme.color; // Using theme card color
final textColor = Theme.of(context).textTheme.bodyLarge?.color;
return SafeArea(
@@ -282,7 +297,8 @@ class HusbandSettingsScreen extends ConsumerWidget {
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.displayMedium?.color ?? AppColors.navyBlue,
color: Theme.of(context).textTheme.displayMedium?.color ??
AppColors.navyBlue,
),
),
const SizedBox(height: 24),
@@ -297,7 +313,8 @@ class HusbandSettingsScreen extends ConsumerWidget {
leading: Icon(Icons.notifications_outlined,
color: Theme.of(context).colorScheme.primary),
title: Text('Notifications',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)),
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500, color: textColor)),
trailing: Switch(value: true, onChanged: (val) {}),
),
const Divider(height: 1),
@@ -305,8 +322,10 @@ class HusbandSettingsScreen extends ConsumerWidget {
leading: Icon(Icons.link,
color: Theme.of(context).colorScheme.primary),
title: Text('Connect with Wife',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)),
trailing: Icon(Icons.chevron_right, color: Theme.of(context).disabledColor),
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500, color: textColor)),
trailing: Icon(Icons.chevron_right,
color: Theme.of(context).disabledColor),
onTap: () => _showConnectDialog(context, ref),
),
const Divider(height: 1),
@@ -314,18 +333,24 @@ class HusbandSettingsScreen extends ConsumerWidget {
leading: Icon(Icons.menu_book_outlined,
color: Theme.of(context).colorScheme.primary),
title: Text('Bible Translation',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)),
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500, color: textColor)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? 'ESV',
ref.watch(userProfileProvider
.select((u) => u?.bibleTranslation.label)) ??
'ESV',
style: GoogleFonts.outfit(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.warmGray,
color:
Theme.of(context).textTheme.bodyMedium?.color ??
AppColors.warmGray,
),
),
Icon(Icons.chevron_right, color: Theme.of(context).disabledColor),
Icon(Icons.chevron_right,
color: Theme.of(context).disabledColor),
],
),
onTap: () => _showTranslationPicker(context, ref),
@@ -335,13 +360,15 @@ class HusbandSettingsScreen extends ConsumerWidget {
leading: Icon(Icons.palette_outlined,
color: Theme.of(context).colorScheme.primary),
title: Text('Appearance',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)),
trailing: Icon(Icons.chevron_right, color: Theme.of(context).disabledColor),
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500, color: textColor)),
trailing: Icon(Icons.chevron_right,
color: Theme.of(context).disabledColor),
onTap: () {
Navigator.push(
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AppearanceScreen(),
builder: (context) => const HusbandAppearanceScreen(),
),
);
},

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,13 @@ import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:uuid/uuid.dart';
import '../../theme/app_theme.dart';
import 'package:christian_period_tracker/models/user_profile.dart';
import 'package:christian_period_tracker/models/cycle_entry.dart';
import '../../models/user_profile.dart';
import '../../models/cycle_entry.dart';
import '../home/home_screen.dart';
import '../husband/husband_home_screen.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/user_provider.dart';
import '../../services/notification_service.dart';
class OnboardingScreen extends ConsumerStatefulWidget {
const OnboardingScreen({super.key});
@@ -35,6 +36,10 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
int _maxCycleLength = 35;
bool _isPadTrackingEnabled = false;
// Connection options
bool _useExampleData = false;
bool _skipPartnerConnection = false;
@override
void dispose() {
_pageController.dispose();
@@ -45,14 +50,15 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
if (_isNavigating) return;
_isNavigating = true;
// Husband Flow: Role (0) -> Name (1) -> Finish
// Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4)
// Husband Flow: Role (0) -> Name (1) -> Connect (2) -> Finish
// Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4) -> [Connect (5) if married]
int nextPage = _currentPage + 1;
// Logic for skipping pages
if (_role == UserRole.husband) {
if (_currentPage == 1) {
if (_currentPage == 2) {
// Finish after connect page
await _completeOnboarding();
return;
}
@@ -63,10 +69,21 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
// Skip fertility goal (page 3) if not married
nextPage = 4;
}
if (_currentPage == 4 &&
_relationshipStatus != RelationshipStatus.married) {
// Skip connect page (page 5) if not married - finish now
await _completeOnboarding();
return;
}
if (_currentPage == 5) {
// Finish after connect page (married wife)
await _completeOnboarding();
return;
}
}
if (nextPage <= 4) {
// Max pages
final maxPages = _role == UserRole.husband ? 2 : 5;
if (nextPage <= maxPages) {
await _pageController.animateToPage(
nextPage,
duration: const Duration(milliseconds: 400),
@@ -124,18 +141,24 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
? _fertilityGoal
: null,
averageCycleLength: _averageCycleLength,
minCycleLength: _minCycleLength,
maxCycleLength: _maxCycleLength,
lastPeriodStartDate: _lastPeriodStart,
isIrregularCycle: _isIrregularCycle,
isPadTrackingEnabled: _isPadTrackingEnabled,
hasCompletedOnboarding: true,
useExampleData: _useExampleData,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
await ref.read(userProfileProvider.notifier).updateProfile(userProfile);
// Trigger partner connection notification if applicable
if (!_skipPartnerConnection && !_useExampleData) {
await NotificationService().showPartnerUpdateNotification(
title: 'Connection Successful!',
body: 'You are now connected with your partner. Tap to start sharing.',
);
}
if (mounted) {
// Navigate to appropriate home screen
if (_role == UserRole.husband) {
@@ -174,7 +197,11 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
padding: const EdgeInsets.all(24),
child: SmoothPageIndicator(
controller: _pageController,
count: isHusband ? 2 : 5,
count: isHusband
? 3
: (_relationshipStatus == RelationshipStatus.married
? 6
: 5),
effect: WormEffect(
dotHeight: 8,
dotWidth: 8,
@@ -190,17 +217,22 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
Expanded(
child: PageView(
controller: _pageController,
physics:
const NeverScrollableScrollPhysics(), // Disable swipe
physics: const NeverScrollableScrollPhysics(), // Disable swipe
onPageChanged: (index) {
setState(() => _currentPage = index);
},
children: [
_buildRolePage(), // Page 0
_buildNamePage(), // Page 1
_buildRelationshipPage(), // Page 2 (Wife only)
_buildFertilityGoalPage(), // Page 3 (Wife married only)
_buildCyclePage(), // Page 4 (Wife only)
if (_role == UserRole.husband)
_buildHusbandConnectPage() // Page 2 (Husband only)
else ...[
_buildRelationshipPage(), // Page 2 (Wife only)
_buildFertilityGoalPage(), // Page 3 (Wife married only)
_buildCyclePage(), // Page 4 (Wife only)
if (_relationshipStatus == RelationshipStatus.married)
_buildWifeConnectPage(), // Page 5 (Wife married only)
],
],
),
),
@@ -223,10 +255,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.blushPink,
AppColors.rose.withOpacity(0.7)
],
colors: [AppColors.blushPink, AppColors.rose.withOpacity(0.7)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
@@ -304,9 +333,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? activeColor
: theme.colorScheme.surfaceVariant,
color:
isSelected ? activeColor : theme.colorScheme.surfaceVariant,
shape: BoxShape.circle,
),
child: Icon(
@@ -340,8 +368,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
],
),
),
if (isSelected)
Icon(Icons.check_circle, color: activeColor),
if (isSelected) Icon(Icons.check_circle, color: activeColor),
],
),
),
@@ -351,8 +378,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
Widget _buildNamePage() {
final theme = Theme.of(context);
final isHusband = _role == UserRole.husband;
final activeColor =
isHusband ? AppColors.navyBlue : AppColors.sageGreen;
final activeColor = isHusband ? AppColors.navyBlue : AppColors.sageGreen;
return Padding(
padding: const EdgeInsets.all(32),
@@ -413,9 +439,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
child: SizedBox(
height: 54,
child: ElevatedButton(
onPressed: (_name.isNotEmpty && !_isNavigating)
? _nextPage
: null,
onPressed:
(_name.isNotEmpty && !_isNavigating) ? _nextPage : null,
style: ElevatedButton.styleFrom(
backgroundColor: activeColor,
),
@@ -557,11 +582,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface)),
const SizedBox(height: 32),
_buildGoalOption(
FertilityGoal.tryingToConceive,
'Trying to Conceive',
'Track fertile days',
Icons.child_care_outlined),
_buildGoalOption(FertilityGoal.tryingToConceive, 'Trying to Conceive',
'Track fertile days', Icons.child_care_outlined),
const SizedBox(height: 12),
_buildGoalOption(
FertilityGoal.tryingToAvoid,
@@ -692,8 +714,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
),
Text('$_averageCycleLength days',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.sageGreen)),
fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
],
),
@@ -720,12 +741,14 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
children: [
Expanded(
child: RangeSlider(
values: RangeValues(_minCycleLength.toDouble(), _maxCycleLength.toDouble()),
values: RangeValues(
_minCycleLength.toDouble(), _maxCycleLength.toDouble()),
min: 21,
max: 45,
divisions: 24,
activeColor: AppColors.sageGreen,
labels: RangeLabels('$_minCycleLength days', '$_maxCycleLength days'),
labels: RangeLabels(
'$_minCycleLength days', '$_maxCycleLength days'),
onChanged: (values) {
setState(() {
_minCycleLength = values.start.round();
@@ -739,8 +762,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
Center(
child: Text('$_minCycleLength - $_maxCycleLength days',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.sageGreen)),
fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
),
],
@@ -840,4 +862,267 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
),
);
}
/// Husband Connect Page - Choose to connect with wife or use example data
Widget _buildHusbandConnectPage() {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.navyBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Icon(Icons.link, size: 32, color: AppColors.navyBlue),
),
const SizedBox(height: 24),
Text(
'Connect with your wife',
style: theme.textTheme.displaySmall?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
'See her cycle info and prayer requests to support her better.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 32),
// Option 1: Connect with Wife (placeholder for now)
_buildConnectOption(
icon: Icons.qr_code_scanner,
title: 'Connect with Wife',
subtitle: 'Enter connection code from her app',
isSelected: !_useExampleData,
onTap: () => setState(() => _useExampleData = false),
color: AppColors.navyBlue,
isDark: isDark,
),
const SizedBox(height: 16),
// Option 2: Use Example Data
_buildConnectOption(
icon: Icons.auto_awesome,
title: 'Use Example Data',
subtitle: 'Explore the app with sample data',
isSelected: _useExampleData,
onTap: () => setState(() => _useExampleData = true),
color: AppColors.navyBlue,
isDark: isDark,
),
const Spacer(),
Row(
children: [
Expanded(
child: SizedBox(
height: 54,
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.navyBlue,
side: const BorderSide(color: AppColors.navyBlue),
),
child: const Text('Back'),
),
),
),
const SizedBox(width: 16),
Expanded(
child: SizedBox(
height: 54,
child: ElevatedButton(
onPressed: !_isNavigating ? _nextPage : null,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
),
child: const Text('Finish Setup'),
),
),
),
],
),
],
),
);
}
/// Wife Connect Page - Invite husband or skip
Widget _buildWifeConnectPage() {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.sageGreen.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Icon(Icons.favorite, size: 32, color: AppColors.sageGreen),
),
const SizedBox(height: 24),
Text(
'Invite your husband',
style: theme.textTheme.displaySmall?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
'Share your cycle info and prayer requests so he can support you.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 32),
// Option 1: Invite Husband
_buildConnectOption(
icon: Icons.share,
title: 'Invite Husband',
subtitle: 'Generate a connection code to share',
isSelected: !_skipPartnerConnection,
onTap: () => setState(() => _skipPartnerConnection = false),
color: AppColors.sageGreen,
isDark: isDark,
),
const SizedBox(height: 16),
// Option 2: Skip for Now
_buildConnectOption(
icon: Icons.schedule,
title: 'Skip for Now',
subtitle: 'You can invite him later in settings',
isSelected: _skipPartnerConnection,
onTap: () => setState(() => _skipPartnerConnection = true),
color: AppColors.sageGreen,
isDark: isDark,
),
const Spacer(),
Row(
children: [
Expanded(
child: SizedBox(
height: 54,
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.sageGreen,
side: const BorderSide(color: AppColors.sageGreen),
),
child: const Text('Back'),
),
),
),
const SizedBox(width: 16),
Expanded(
child: SizedBox(
height: 54,
child: ElevatedButton(
onPressed: !_isNavigating ? _nextPage : null,
child: const Text('Get Started'),
),
),
),
],
),
],
),
);
}
/// Helper for connection option cards
Widget _buildConnectOption({
required IconData icon,
required String title,
required String subtitle,
required bool isSelected,
required VoidCallback onTap,
required Color color,
required bool isDark,
}) {
final theme = Theme.of(context);
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? color.withOpacity(isDark ? 0.3 : 0.1)
: theme.cardTheme.color,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
isSelected ? color : theme.colorScheme.outline.withOpacity(0.1),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? color : theme.colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: isSelected
? Colors.white
: theme.colorScheme.onSurfaceVariant,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
if (isSelected) Icon(Icons.check_circle, color: color),
],
),
),
);
}
}