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

@@ -7,10 +7,11 @@ plugins {
android {
namespace = "com.faithapps.christian_period_tracker"
compileSdk = flutter.compileSdkVersion
compileSdk = 36
ndkVersion = flutter.ndkVersion
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
@@ -24,7 +25,7 @@ android {
applicationId = "com.faithapps.christian_period_tracker"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
@@ -39,6 +40,17 @@ android {
}
}
configurations.all {
resolutionStrategy {
force 'androidx.core:core:1.13.1'
force 'androidx.core:core-ktx:1.13.1'
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
}
flutter {
source = "../.."
}

View File

@@ -1,14 +1,5 @@
<<<<<<< HEAD
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
=======
#Fri Dec 19 21:26:00 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
>>>>>>> 6742220 (Your commit message here)

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}

View File

@@ -27,6 +27,9 @@ enum MoodLevel {
/// Flow intensity for period days
@HiveType(typeId: 4)
enum FlowIntensity {
@HiveField(4)
none, // No flow / Precautionary
@HiveField(0)
spotting,
@@ -166,7 +169,8 @@ class CycleEntry extends HiveObject {
String? husbandNotes; // Separate notes for husband
@HiveField(29)
bool? intimacyProtected; // null = no selection, true = protected, false = unprotected
bool?
intimacyProtected; // null = no selection, true = protected, false = unprotected
@HiveField(30, defaultValue: false)
bool usedPantyliner;
@@ -174,6 +178,9 @@ class CycleEntry extends HiveObject {
@HiveField(31, defaultValue: 0)
int pantylinerCount;
@HiveField(32)
String? prayerRequest;
CycleEntry({
required this.id,
required this.date,
@@ -207,6 +214,7 @@ class CycleEntry extends HiveObject {
this.husbandNotes,
this.usedPantyliner = false,
this.pantylinerCount = 0,
this.prayerRequest,
});
List<bool> get _symptomsList => [
@@ -271,6 +279,7 @@ class CycleEntry extends HiveObject {
String? husbandNotes,
bool? usedPantyliner,
int? pantylinerCount,
String? prayerRequest,
}) {
return CycleEntry(
id: id ?? this.id,
@@ -292,7 +301,8 @@ class CycleEntry extends HiveObject {
hasInsomnia: hasInsomnia ?? this.hasInsomnia,
basalBodyTemperature: basalBodyTemperature ?? this.basalBodyTemperature,
cervicalMucus: cervicalMucus ?? this.cervicalMucus,
ovulationTestPositive: ovulationTestPositive ?? this.ovulationTestPositive,
ovulationTestPositive:
ovulationTestPositive ?? this.ovulationTestPositive,
notes: notes ?? this.notes,
cravings: cravings ?? this.cravings,
sleepHours: sleepHours ?? this.sleepHours,
@@ -305,6 +315,7 @@ class CycleEntry extends HiveObject {
husbandNotes: husbandNotes ?? this.husbandNotes,
usedPantyliner: usedPantyliner ?? this.usedPantyliner,
pantylinerCount: pantylinerCount ?? this.pantylinerCount,
prayerRequest: prayerRequest ?? this.prayerRequest,
);
}
}
@@ -345,6 +356,8 @@ extension MoodLevelExtension on MoodLevel {
extension FlowIntensityExtension on FlowIntensity {
String get label {
switch (this) {
case FlowIntensity.none:
return 'No Flow';
case FlowIntensity.spotting:
return 'Spotting';
case FlowIntensity.light:
@@ -410,11 +423,7 @@ extension CyclePhaseExtension on CyclePhase {
case CyclePhase.ovulation:
return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose];
case CyclePhase.luteal:
return [
AppColors.lutealPhase,
AppColors.lavender,
AppColors.blushPink
];
return [AppColors.lutealPhase, AppColors.lavender, AppColors.blushPink];
}
}

View File

@@ -49,13 +49,14 @@ class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
husbandNotes: fields[28] as String?,
usedPantyliner: fields[30] == null ? false : fields[30] as bool,
pantylinerCount: fields[31] == null ? 0 : fields[31] as int,
prayerRequest: fields[32] as String?,
);
}
@override
void write(BinaryWriter writer, CycleEntry obj) {
writer
..writeByte(32)
..writeByte(33)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -119,7 +120,9 @@ class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
..writeByte(30)
..write(obj.usedPantyliner)
..writeByte(31)
..write(obj.pantylinerCount);
..write(obj.pantylinerCount)
..writeByte(32)
..write(obj.prayerRequest);
}
@override
@@ -194,6 +197,8 @@ class FlowIntensityAdapter extends TypeAdapter<FlowIntensity> {
@override
FlowIntensity read(BinaryReader reader) {
switch (reader.readByte()) {
case 4:
return FlowIntensity.none;
case 0:
return FlowIntensity.spotting;
case 1:
@@ -203,13 +208,16 @@ class FlowIntensityAdapter extends TypeAdapter<FlowIntensity> {
case 3:
return FlowIntensity.heavy;
default:
return FlowIntensity.spotting;
return FlowIntensity.none;
}
}
@override
void write(BinaryWriter writer, FlowIntensity obj) {
switch (obj) {
case FlowIntensity.none:
writer.writeByte(4);
break;
case FlowIntensity.spotting:
writer.writeByte(0);
break;

View File

@@ -302,6 +302,17 @@ class UserProfile extends HiveObject {
@HiveField(53)
List<TeachingPlan>? teachingPlans;
// Husband-specific theme settings
@HiveField(54, defaultValue: AppThemeMode.system)
AppThemeMode husbandThemeMode;
@HiveField(55, defaultValue: '0xFF1A3A5C')
String husbandAccentColor;
// Whether to use example/demo data (for husband not connected to wife)
@HiveField(56, defaultValue: false)
bool useExampleData;
UserProfile({
required this.id,
required this.name,
@@ -356,6 +367,9 @@ class UserProfile extends HiveObject {
this.isCalendarProtected = false,
this.isSuppliesProtected = false,
this.teachingPlans,
this.husbandThemeMode = AppThemeMode.system,
this.husbandAccentColor = '0xFF1A3A5C',
this.useExampleData = false,
});
/// Check if user is married
@@ -435,6 +449,9 @@ class UserProfile extends HiveObject {
bool? isCalendarProtected,
bool? isSuppliesProtected,
List<TeachingPlan>? teachingPlans,
AppThemeMode? husbandThemeMode,
String? husbandAccentColor,
bool? useExampleData,
}) {
return UserProfile(
id: id ?? this.id,
@@ -470,8 +487,10 @@ class UserProfile extends HiveObject {
padBrand: padBrand ?? this.padBrand,
padAbsorbency: padAbsorbency ?? this.padAbsorbency,
padInventoryCount: padInventoryCount ?? this.padInventoryCount,
lowInventoryThreshold: lowInventoryThreshold ?? this.lowInventoryThreshold,
isAutoInventoryEnabled: isAutoInventoryEnabled ?? this.isAutoInventoryEnabled,
lowInventoryThreshold:
lowInventoryThreshold ?? this.lowInventoryThreshold,
isAutoInventoryEnabled:
isAutoInventoryEnabled ?? this.isAutoInventoryEnabled,
lastInventoryUpdate: lastInventoryUpdate ?? this.lastInventoryUpdate,
notifyPeriodEstimate: notifyPeriodEstimate ?? this.notifyPeriodEstimate,
notifyPeriodStart: notifyPeriodStart ?? this.notifyPeriodStart,
@@ -491,6 +510,9 @@ class UserProfile extends HiveObject {
isCalendarProtected: isCalendarProtected ?? this.isCalendarProtected,
isSuppliesProtected: isSuppliesProtected ?? this.isSuppliesProtected,
teachingPlans: teachingPlans ?? this.teachingPlans,
husbandThemeMode: husbandThemeMode ?? this.husbandThemeMode,
husbandAccentColor: husbandAccentColor ?? this.husbandAccentColor,
useExampleData: useExampleData ?? this.useExampleData,
);
}
}

View File

@@ -118,13 +118,18 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
isCalendarProtected: fields[51] == null ? false : fields[51] as bool,
isSuppliesProtected: fields[52] == null ? false : fields[52] as bool,
teachingPlans: (fields[53] as List?)?.cast<TeachingPlan>(),
husbandThemeMode:
fields[54] == null ? AppThemeMode.system : fields[54] as AppThemeMode,
husbandAccentColor:
fields[55] == null ? '0xFF1A3A5C' : fields[55] as String,
useExampleData: fields[56] == null ? false : fields[56] as bool,
);
}
@override
void write(BinaryWriter writer, UserProfile obj) {
writer
..writeByte(53)
..writeByte(56)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -230,7 +235,13 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
..writeByte(52)
..write(obj.isSuppliesProtected)
..writeByte(53)
..write(obj.teachingPlans);
..write(obj.teachingPlans)
..writeByte(54)
..write(obj.husbandThemeMode)
..writeByte(55)
..write(obj.husbandAccentColor)
..writeByte(56)
..write(obj.useExampleData);
}
@override

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

View File

@@ -34,12 +34,13 @@ class NotificationService {
requestBadgePermission: true,
requestSoundPermission: true,
);
// Linux initialization (optional, but good for completeness)
final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification');
final InitializationSettings initializationSettings = InitializationSettings(
final InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin,
macOS: initializationSettingsDarwin,
@@ -63,10 +64,10 @@ class NotificationService {
required DateTime scheduledDate,
}) async {
if (kIsWeb) {
// Web platform limitation: Background scheduling is complex.
// For this demo/web preview, we'll just log it or rely on the UI confirmation.
print('Web Notification Scheduled: $title - $body at $scheduledDate');
return;
// Web platform limitation: Background scheduling is complex.
// For this demo/web preview, we'll just log it or rely on the UI confirmation.
print('Web Notification Scheduled: $title - $body at $scheduledDate');
return;
}
await flutterLocalNotificationsPlugin.zonedSchedule(
@@ -92,30 +93,72 @@ class NotificationService {
// New method for specific notification types
Future<void> showLocalNotification({
required int id,
required String title,
required String body,
String? channelId,
String? channelName,
required int id,
required String title,
required String body,
String? channelId,
String? channelName,
}) async {
if (kIsWeb) {
print('Web Local Notification: $title - $body');
return;
}
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'tracker_general', 'General Notifications',
AndroidNotificationDetails('tracker_general', 'General Notifications',
channelDescription: 'General app notifications',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker');
const NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
await flutterLocalNotificationsPlugin.show(
id, title, body, notificationDetails,
payload: 'item x');
await flutterLocalNotificationsPlugin
.show(id, title, body, notificationDetails, payload: 'item x');
}
Future<void> showPrayerRequestNotification(
{required String senderName}) async {
await showLocalNotification(
id: 300,
title: 'New Prayer Request',
body: '$senderName sent you a prayer request. Tap to pray with them.',
);
}
Future<void> showTeachingPlanNotification(
{required String teacherName}) async {
await showLocalNotification(
id: 302,
title: 'New Teaching Plan',
body: '$teacherName added a new teaching plan for you.',
);
}
Future<void> showPartnerUpdateNotification(
{required String title, required String body}) async {
await showLocalNotification(
id: 305,
title: title,
body: body,
);
}
Future<void> showCycleUpdateNotification({required String message}) async {
await showLocalNotification(
id: 310,
title: 'Cycle Update',
body: message,
);
}
Future<void> showSymptomNotification(
{required String senderName, required String symptom}) async {
await showLocalNotification(
id: 315,
title: 'Partner Care Reminder',
body: '$senderName logged $symptom. A little extra care might be nice!',
);
}
Future<void> cancelNotification(int id) async {

View File

@@ -17,6 +17,9 @@ class PadTrackerCard extends ConsumerStatefulWidget {
class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
Timer? _timer;
String _timeDisplay = '';
double _progress = 0.0;
Color _statusColor = AppColors.menstrualPhase;
bool _isCountDown = true; // Toggle state
@override
void initState() {
@@ -40,35 +43,92 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
void _updateTime() {
final user = ref.read(userProfileProvider);
if (user?.lastPadChangeTime == null) {
if (mounted) setState(() => _timeDisplay = 'Tap to start');
if (mounted) {
setState(() {
_timeDisplay = 'Tap to start';
_progress = 0;
_statusColor = AppColors.menstrualPhase;
});
}
return;
}
final now = DateTime.now();
final difference = now.difference(user!.lastPadChangeTime!);
// We want to show time SINCE change (duration worn)
final hours = difference.inHours;
final minutes = difference.inMinutes.remainder(60);
final seconds = difference.inSeconds.remainder(60);
// Estimate max duration based on flow
// None/Precautionary: 8h, Spotting: 8h, Light: 6h, Medium: 4h, Heavy: 3h
final flowIntensity = user.typicalFlowIntensity ?? 2; // Default to light
Duration maxDuration;
switch (flowIntensity) {
case 0: // No Flow / Precautionary (health guideline: 8h max)
maxDuration = const Duration(hours: 8);
break;
case 1: // Spotting
maxDuration = const Duration(hours: 8);
break;
case 2: // Light
maxDuration = const Duration(hours: 6);
break;
case 3: // Medium
maxDuration = const Duration(hours: 4);
break;
case 4: // Heavy
maxDuration = const Duration(hours: 3);
break;
case 5: // Very Heavy
maxDuration = const Duration(hours: 2);
break;
default:
maxDuration = const Duration(hours: 4);
}
final totalSeconds = maxDuration.inSeconds;
final elapsedSeconds = difference.inSeconds;
double progress = elapsedSeconds / totalSeconds;
progress = progress.clamp(0.0, 1.0);
// Determine Status Color
Color newColor = AppColors.menstrualPhase;
if (progress > 0.9) {
newColor = Colors.red;
} else if (progress > 0.75) {
newColor = Colors.orange;
} else {
newColor = AppColors.menstrualPhase; // Greenish/Theme color
}
// Override if we want to visually show "fresh" vs "old"
String text = '';
if (user.showPadTimerMinutes) {
if (hours > 0) text += '$hours hr ';
text += '$minutes min';
}
if (user.showPadTimerSeconds) {
if (text.isNotEmpty) text += ' ';
text += '$seconds sec';
}
if (text.isEmpty) text = 'Active'; // Fallback
if (_isCountDown) {
final remaining = maxDuration - difference;
final isOverdue = remaining.isNegative;
final absRemaining = remaining.abs();
final hours = absRemaining.inHours;
final mins = absRemaining.inMinutes % 60;
if (isOverdue) {
text = 'Overdue by ${hours}h ${mins}m';
newColor = Colors.red; // Force red if overdue
} else {
text = '${hours}h ${mins}m left';
}
_progress =
isOverdue ? 1.0 : (1.0 - progress); // Depleting if not overdue
} else {
// Count Up
final hours = difference.inHours;
final minutes = difference.inMinutes % 60;
text = '${hours}h ${minutes}m worn';
_progress = progress; // Filling
}
if (mounted) {
setState(() {
_timeDisplay = text;
_progress = _isCountDown && text.contains('Overdue') ? 1.0 : _progress;
_statusColor = newColor;
});
}
}
@@ -76,10 +136,8 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
@override
Widget build(BuildContext context) {
final user = ref.watch(userProfileProvider);
if (user == null || !user.isPadTrackingEnabled) return const SizedBox.shrink();
// Re-check time on rebuilds in case settings changed
// _updateTime(); // Actually let the timer handle it, or use a key to rebuild on setting changes
if (user == null || !user.isPadTrackingEnabled)
return const SizedBox.shrink();
return GestureDetector(
onTap: () {
@@ -91,53 +149,86 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.menstrualPhase.withOpacity(0.1),
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)),
border: Border.all(color: _statusColor.withOpacity(0.3)),
boxShadow: [
BoxShadow(
color: AppColors.menstrualPhase.withOpacity(0.05),
blurRadius: 8,
BoxShadow(
color: _statusColor.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.menstrualPhase.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(Icons.timer_outlined, color: AppColors.menstrualPhase, size: 24),
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: _statusColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child:
Icon(Icons.timer_outlined, color: _statusColor, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Pad Tracker',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
InkWell(
onTap: () {
setState(() {
_isCountDown = !_isCountDown;
_updateTime();
});
},
child: Icon(
_isCountDown
? Icons.arrow_downward
: Icons.arrow_upward,
size: 16,
color: AppColors.warmGray),
)
],
),
const SizedBox(height: 4),
Text(
_timeDisplay.isNotEmpty ? _timeDisplay : 'Tap to track',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _statusColor),
),
],
),
),
const Icon(Icons.chevron_right, color: AppColors.lightGray),
],
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pad Tracker',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 4),
Text(
_timeDisplay.isNotEmpty ? _timeDisplay : 'Tap to track',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.menstrualPhase
),
),
],
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: _progress,
backgroundColor: _statusColor.withOpacity(0.1),
valueColor: AlwaysStoppedAnimation<Color>(_statusColor),
minHeight: 6,
),
),
const Icon(Icons.chevron_right, color: AppColors.menstrualPhase),
],
),
),

View File

@@ -4,6 +4,7 @@ import 'package:google_fonts/google_fonts.dart';
import '../providers/user_provider.dart';
import '../theme/app_theme.dart';
import '../providers/navigation_provider.dart';
import '../models/user_profile.dart';
import 'quick_log_dialog.dart';
class QuickLogButtons extends ConsumerWidget {
@@ -63,6 +64,13 @@ class QuickLogButtons extends ConsumerWidget {
color: AppColors.lutealPhase,
onTap: () => _showQuickLogDialog(context, 'pads'),
),
_buildQuickButton(
context,
icon: Icons.church_outlined,
label: 'Prayer',
color: AppColors.softGold, // Or a suitable color
onTap: () => _showQuickLogDialog(context, 'prayer'),
),
],
),
);
@@ -96,8 +104,7 @@ class QuickLogButtons extends ConsumerWidget {
decoration: BoxDecoration(
color: color.withOpacity(isDark ? 0.2 : 0.15),
borderRadius: BorderRadius.circular(12),
border:
isDark ? Border.all(color: color.withOpacity(0.3)) : null,
border: isDark ? Border.all(color: color.withOpacity(0.3)) : null,
),
child: Column(
mainAxisSize: MainAxisSize.min,

View File

@@ -23,7 +23,7 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
FlowIntensity? _flowIntensity;
MoodLevel? _mood;
int? _energyLevel;
// Symptoms & Cravings
final Map<String, bool> _symptoms = {
'Headache': false,
@@ -37,7 +37,7 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
'Insomnia': false,
'Cramps': false,
};
final TextEditingController _cravingController = TextEditingController();
List<String> _cravings = [];
List<String> _recentCravings = [];
@@ -102,6 +102,8 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
return _buildSymptomsLog();
case 'cravings':
return _buildCravingsLog();
case 'prayer':
return _buildPrayerLog();
default:
return const Text('Invalid log type.');
}
@@ -137,7 +139,7 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
}
Widget _buildCravingsLog() {
return Container(
return Container(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -152,37 +154,44 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
),
onSubmitted: (value) {
if (value.isNotEmpty) {
setState(() {
_cravings.add(value.trim());
_cravingController.clear();
});
setState(() {
_cravings.add(value.trim());
_cravingController.clear();
});
}
},
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: _cravings.map((c) => Chip(
label: Text(c),
onDeleted: () {
setState(() => _cravings.remove(c));
},
)).toList(),
children: _cravings
.map((c) => Chip(
label: Text(c),
onDeleted: () {
setState(() => _cravings.remove(c));
},
))
.toList(),
),
const SizedBox(height: 16),
if (_recentCravings.isNotEmpty) ...[
Text('Recent Cravings:', style: GoogleFonts.outfit(fontSize: 12, fontWeight: FontWeight.bold)),
Text('Recent Cravings:',
style: GoogleFonts.outfit(
fontSize: 12, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Wrap(
Wrap(
spacing: 8,
children: _recentCravings.take(5).map((c) => ActionChip(
label: Text(c),
onPressed: () {
if (!_cravings.contains(c)) {
setState(() => _cravings.add(c));
}
},
)).toList(),
children: _recentCravings
.take(5)
.map((c) => ActionChip(
label: Text(c),
onPressed: () {
if (!_cravings.contains(c)) {
setState(() => _cravings.add(c));
}
},
))
.toList(),
),
]
],
@@ -277,10 +286,30 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
);
}
Widget _buildPrayerLog() {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter your prayer request or gratitude:'),
const SizedBox(height: 16),
TextField(
controller:
_cravingController, // Reusing controller for simplicity, or create _prayerController
maxLines: 4,
decoration: const InputDecoration(
hintText: 'I am thankful for...',
border: OutlineInputBorder(),
),
),
],
);
}
Future<void> _saveLog() async {
// Handle text input for cravings if user didn't hit enter
if (widget.logType == 'cravings' && _cravingController.text.isNotEmpty) {
_cravings.add(_cravingController.text.trim());
_cravings.add(_cravingController.text.trim());
}
final cycleNotifier = ref.read(cycleEntriesProvider.notifier);
@@ -288,7 +317,11 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
final entries = ref.read(cycleEntriesProvider);
final entry = entries.firstWhere(
(e) => DateUtils.isSameDay(e.date, today),
orElse: () => CycleEntry(id: const Uuid().v4(), date: today, createdAt: today, updatedAt: today),
orElse: () => CycleEntry(
id: const Uuid().v4(),
date: today,
createdAt: today,
updatedAt: today),
);
CycleEntry updatedEntry = entry;
@@ -308,28 +341,61 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
break;
case 'symptoms':
updatedEntry = entry.copyWith(
hasHeadache: _symptoms['Headache'],
hasBloating: _symptoms['Bloating'],
hasBreastTenderness: _symptoms['Breast Tenderness'],
hasFatigue: _symptoms['Fatigue'],
hasAcne: _symptoms['Acne'],
hasLowerBackPain: _symptoms['Back Pain'],
hasConstipation: _symptoms['Constipation'],
hasDiarrhea: _symptoms['Diarrhea'],
hasInsomnia: _symptoms['Insomnia'],
crampIntensity: _symptoms['Cramps'] == true ? 2 : 0, // Default to mild cramps if just toggled
hasHeadache: _symptoms['Headache'],
hasBloating: _symptoms['Bloating'],
hasBreastTenderness: _symptoms['Breast Tenderness'],
hasFatigue: _symptoms['Fatigue'],
hasAcne: _symptoms['Acne'],
hasLowerBackPain: _symptoms['Back Pain'],
hasConstipation: _symptoms['Constipation'],
hasDiarrhea: _symptoms['Diarrhea'],
hasInsomnia: _symptoms['Insomnia'],
crampIntensity: _symptoms['Cramps'] == true
? 2
: 0, // Default to mild cramps if just toggled
);
// Trigger notification if any symptom is selected
final user = ref.read(userProfileProvider);
if (_symptoms.values.any((selected) => selected == true)) {
final selectedSymptom = _symptoms.entries
.firstWhere((element) => element.value == true)
.key;
NotificationService().showSymptomNotification(
senderName: user?.name ?? 'Wife',
symptom: selectedSymptom,
);
}
break;
case 'cravings':
final currentCravings = entry.cravings ?? [];
final newCravings = {...currentCravings, ..._cravings}.toList();
updatedEntry = entry.copyWith(cravings: newCravings);
// Update History
final prefs = await SharedPreferences.getInstance();
final history = prefs.getStringList('recent_cravings') ?? [];
final updatedHistory = {..._cravings, ...history}.take(20).toList();
await prefs.setStringList('recent_cravings', updatedHistory);
await prefs.setStringList('recent_cravings', updatedHistory);
break;
case 'prayer':
final currentPrayer = entry.prayerRequest ?? '';
final newPrayer =
_cravingController.text.trim(); // Using reused controller
if (newPrayer.isNotEmpty) {
updatedEntry = entry.copyWith(
prayerRequest: currentPrayer.isEmpty
? newPrayer
: '$currentPrayer\n$newPrayer');
// Trigger notification
final user = ref.read(userProfileProvider);
NotificationService().showPrayerRequestNotification(
senderName: user?.name ?? 'Wife',
);
} else {
return; // Don't save empty prayer
}
break;
default:
// pads handled separately