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 { android {
namespace = "com.faithapps.christian_period_tracker" namespace = "com.faithapps.christian_period_tracker"
compileSdk = flutter.compileSdkVersion compileSdk = 36
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
@@ -24,7 +25,7 @@ android {
applicationId = "com.faithapps.christian_period_tracker" applicationId = "com.faithapps.christian_period_tracker"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = 26
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName 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 { flutter {
source = "../.." 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
>>>>>>> 6742220 (Your commit message here)

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" 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 id "org.jetbrains.kotlin.android" version "1.8.22" apply false
} }

View File

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

View File

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

View File

@@ -302,6 +302,17 @@ class UserProfile extends HiveObject {
@HiveField(53) @HiveField(53)
List<TeachingPlan>? teachingPlans; 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({ UserProfile({
required this.id, required this.id,
required this.name, required this.name,
@@ -356,6 +367,9 @@ class UserProfile extends HiveObject {
this.isCalendarProtected = false, this.isCalendarProtected = false,
this.isSuppliesProtected = false, this.isSuppliesProtected = false,
this.teachingPlans, this.teachingPlans,
this.husbandThemeMode = AppThemeMode.system,
this.husbandAccentColor = '0xFF1A3A5C',
this.useExampleData = false,
}); });
/// Check if user is married /// Check if user is married
@@ -435,6 +449,9 @@ class UserProfile extends HiveObject {
bool? isCalendarProtected, bool? isCalendarProtected,
bool? isSuppliesProtected, bool? isSuppliesProtected,
List<TeachingPlan>? teachingPlans, List<TeachingPlan>? teachingPlans,
AppThemeMode? husbandThemeMode,
String? husbandAccentColor,
bool? useExampleData,
}) { }) {
return UserProfile( return UserProfile(
id: id ?? this.id, id: id ?? this.id,
@@ -470,8 +487,10 @@ class UserProfile extends HiveObject {
padBrand: padBrand ?? this.padBrand, padBrand: padBrand ?? this.padBrand,
padAbsorbency: padAbsorbency ?? this.padAbsorbency, padAbsorbency: padAbsorbency ?? this.padAbsorbency,
padInventoryCount: padInventoryCount ?? this.padInventoryCount, padInventoryCount: padInventoryCount ?? this.padInventoryCount,
lowInventoryThreshold: lowInventoryThreshold ?? this.lowInventoryThreshold, lowInventoryThreshold:
isAutoInventoryEnabled: isAutoInventoryEnabled ?? this.isAutoInventoryEnabled, lowInventoryThreshold ?? this.lowInventoryThreshold,
isAutoInventoryEnabled:
isAutoInventoryEnabled ?? this.isAutoInventoryEnabled,
lastInventoryUpdate: lastInventoryUpdate ?? this.lastInventoryUpdate, lastInventoryUpdate: lastInventoryUpdate ?? this.lastInventoryUpdate,
notifyPeriodEstimate: notifyPeriodEstimate ?? this.notifyPeriodEstimate, notifyPeriodEstimate: notifyPeriodEstimate ?? this.notifyPeriodEstimate,
notifyPeriodStart: notifyPeriodStart ?? this.notifyPeriodStart, notifyPeriodStart: notifyPeriodStart ?? this.notifyPeriodStart,
@@ -491,6 +510,9 @@ class UserProfile extends HiveObject {
isCalendarProtected: isCalendarProtected ?? this.isCalendarProtected, isCalendarProtected: isCalendarProtected ?? this.isCalendarProtected,
isSuppliesProtected: isSuppliesProtected ?? this.isSuppliesProtected, isSuppliesProtected: isSuppliesProtected ?? this.isSuppliesProtected,
teachingPlans: teachingPlans ?? this.teachingPlans, 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, isCalendarProtected: fields[51] == null ? false : fields[51] as bool,
isSuppliesProtected: fields[52] == null ? false : fields[52] as bool, isSuppliesProtected: fields[52] == null ? false : fields[52] as bool,
teachingPlans: (fields[53] as List?)?.cast<TeachingPlan>(), 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 @override
void write(BinaryWriter writer, UserProfile obj) { void write(BinaryWriter writer, UserProfile obj) {
writer writer
..writeByte(53) ..writeByte(56)
..writeByte(0) ..writeByte(0)
..write(obj.id) ..write(obj.id)
..writeByte(1) ..writeByte(1)
@@ -230,7 +235,13 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
..writeByte(52) ..writeByte(52)
..write(obj.isSuppliesProtected) ..write(obj.isSuppliesProtected)
..writeByte(53) ..writeByte(53)
..write(obj.teachingPlans); ..write(obj.teachingPlans)
..writeByte(54)
..write(obj.husbandThemeMode)
..writeByte(55)
..write(obj.husbandAccentColor)
..writeByte(56)
..write(obj.useExampleData);
} }
@override @override

View File

@@ -37,7 +37,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
final entries = ref.watch(cycleEntriesProvider); final entries = ref.watch(cycleEntriesProvider);
final user = ref.watch(userProfileProvider); final user = ref.watch(userProfileProvider);
final isIrregular = user?.isIrregularCycle ?? false; final isIrregular = user?.isIrregularCycle ?? false;
int cycleLength = user?.averageCycleLength ?? 28; int cycleLength = user?.averageCycleLength ?? 28;
if (isIrregular) { if (isIrregular) {
if (_predictionMode == PredictionMode.short) { if (_predictionMode == PredictionMode.short) {
@@ -46,237 +46,257 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
cycleLength = user?.maxCycleLength ?? 35; cycleLength = user?.maxCycleLength ?? 35;
} }
} }
final lastPeriodStart = user?.lastPeriodStartDate; final lastPeriodStart = user?.lastPeriodStartDate;
return ProtectedContentWrapper( return ProtectedContentWrapper(
title: 'Calendar', title: 'Calendar',
isProtected: user?.isCalendarProtected ?? false, isProtected: user?.isCalendarProtected ?? false,
userProfile: user, userProfile: user,
child: SafeArea( child: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
// Header // Header
Padding( Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Column( child: Column(
children: [
Row(
children: [ children: [
Expanded( Row(
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,
children: [ children: [
if (entry.isPeriodDay) Expanded(
Container( child: Text(
width: 6, 'Calendar',
height: 6, style: GoogleFonts.outfit(
margin: const EdgeInsets.symmetric(horizontal: 1), fontSize: 28,
decoration: const BoxDecoration( fontWeight: FontWeight.w600,
color: AppColors.menstrualPhase, color: Theme.of(context)
shape: BoxShape.circle, .textTheme
), .headlineLarge
), ?.color,
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,
), ),
), ),
),
_buildLegendButton(),
], ],
), ),
); if (isIrregular) ...[
}, const SizedBox(height: 16),
_buildPredictionToggle(),
],
],
),
), ),
),
),
const SizedBox(height: 24), // Calendar
Container(
// Divider / Header for Day Info margin: const EdgeInsets.symmetric(horizontal: 16),
if (_selectedDay != null) ...[ decoration: BoxDecoration(
Padding( color: Theme.of(context).cardColor,
padding: const EdgeInsets.symmetric(horizontal: 20), borderRadius: BorderRadius.circular(20),
child: Row( boxShadow: [
children: [ BoxShadow(
Text( color: Colors.black.withOpacity(0.05),
'Daily Log', blurRadius: 15,
style: GoogleFonts.outfit( offset: const Offset(0, 5),
fontSize: 16, ),
],
),
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, fontWeight: FontWeight.w600,
color: AppColors.warmGray, color: AppColors.sageGreen,
letterSpacing: 1, ),
selectedDecoration: const BoxDecoration(
color: AppColors.sageGreen,
shape: BoxShape.circle,
),
selectedTextStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
), ),
), ),
const SizedBox(width: 12), headerStyle: HeaderStyle(
const Expanded(child: Divider(color: AppColors.lightGray)), 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), const SizedBox(height: 24),
// Day Info (No longer Expanded) // Divider / Header for Day Info
_buildDayInfo( if (_selectedDay != null) ...[
_selectedDay!, lastPeriodStart, cycleLength, entries, user), Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
const SizedBox(height: 40), // Bottom padding 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() { Widget _buildPredictionToggle() {
@@ -288,9 +308,12 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
), ),
child: Row( child: Row(
children: [ children: [
_buildToggleItem(PredictionMode.short, 'Short (-)', AppColors.menstrualPhase), _buildToggleItem(
_buildToggleItem(PredictionMode.regular, 'Regular', AppColors.sageGreen), PredictionMode.short, 'Short (-)', AppColors.menstrualPhase),
_buildToggleItem(PredictionMode.long, 'Long (+)', AppColors.lutealPhase), _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, Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart,
List<CycleEntry> entries, UserProfile? user) { int cycleLength, List<CycleEntry> entries, UserProfile? user) {
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength); final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
final entry = _getEntryForDate(date, entries); final entry = _getEntryForDate(date, entries);
@@ -487,17 +510,18 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
.bodyMedium .bodyMedium
?.copyWith(color: AppColors.warmGray), ?.copyWith(color: AppColors.warmGray),
), ),
if (user?.isPadTrackingEnabled == true && if (user?.isPadTrackingEnabled == true &&
phase != CyclePhase.menstrual && phase != CyclePhase.menstrual &&
(user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ?? false)) ...[ (user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ??
false)) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
_buildPantylinerPrompt(date, null), _buildPantylinerPrompt(date, null),
], ],
] else ...[ ] else ...[
// Period Detail // Period Detail
if (entry.isPeriodDay) if (entry.isPeriodDay)
_buildDetailRow(Icons.water_drop, 'Period Day', _buildDetailRow(
AppColors.menstrualPhase, Icons.water_drop, 'Period Day', AppColors.menstrualPhase,
value: entry.flowIntensity?.label), value: entry.flowIntensity?.label),
// Mood Detail // Mood Detail
@@ -524,11 +548,11 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
if (user?.isPadTrackingEnabled == true) ...[ if (user?.isPadTrackingEnabled == true) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
if (entry.usedPantyliner) if (entry.usedPantyliner)
_buildDetailRow(Icons.layers_outlined, 'Supplies Used', AppColors.menstrualPhase, _buildDetailRow(Icons.layers_outlined, 'Supplies Used',
AppColors.menstrualPhase,
value: '${entry.pantylinerCount}'), value: '${entry.pantylinerCount}'),
if (!entry.usedPantyliner && !entry.isPeriodDay) if (!entry.usedPantyliner && !entry.isPeriodDay)
_buildPantylinerPrompt(date, entry), _buildPantylinerPrompt(date, entry),
], ],
// Notes // Notes
@@ -544,16 +568,15 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.warmGray)), color: AppColors.warmGray)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text(entry.notes!, Text(entry.notes!, style: GoogleFonts.outfit(fontSize: 14)),
style: GoogleFonts.outfit(fontSize: 14)),
], ],
), ),
), ),
], ],
if (user?.isPadTrackingEnabled == true) ...[ if (user?.isPadTrackingEnabled == true) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
_buildManualSupplyEntryButton(date), _buildManualSupplyEntryButton(date),
], ],
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -581,17 +604,17 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
? Icons.edit_note ? Icons.edit_note
: Icons.add_circle_outline), : Icons.add_circle_outline),
label: Text(entry != null ? 'Edit Log' : 'Add Log'), label: Text(entry != null ? 'Edit Log' : 'Add Log'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.sageGreen, backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)), borderRadius: BorderRadius.circular(12)),
),
), ),
), ),
), ],
], ),
),
], ],
), ),
); );
@@ -608,33 +631,38 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
), ),
child: Row( child: Row(
children: [ 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), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'Did you use pantyliners today?', 'Did you use pantyliners today?',
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal), style:
GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal),
), ),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
if (entry != null) { if (entry != null) {
ref.read(cycleEntriesProvider.notifier).updateEntry( ref.read(cycleEntriesProvider.notifier).updateEntry(
entry.copyWith(usedPantyliner: true, pantylinerCount: 1), entry.copyWith(usedPantyliner: true, pantylinerCount: 1),
); );
} else { } else {
final newEntry = CycleEntry( final newEntry = CycleEntry(
id: const Uuid().v4(), id: const Uuid().v4(),
date: date, date: date,
usedPantyliner: true, usedPantyliner: true,
pantylinerCount: 1, pantylinerCount: 1,
createdAt: DateTime.now(), createdAt: DateTime.now(),
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
ref.read(cycleEntriesProvider.notifier).addEntry(newEntry); 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( style: OutlinedButton.styleFrom(
foregroundColor: AppColors.menstrualPhase, foregroundColor: AppColors.menstrualPhase,
side: const BorderSide(color: 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; return entry?.isPeriodDay ?? false;
} }
Widget _buildCalendarDay( Widget _buildCalendarDay(DateTime day, DateTime focusedDay,
DateTime day, List<CycleEntry> entries, DateTime? lastPeriodStart, int cycleLength,
DateTime focusedDay,
List<CycleEntry> entries,
DateTime? lastPeriodStart,
int cycleLength,
{bool isSelected = false, bool isToday = false, bool isWeekend = false}) { {bool isSelected = false, bool isToday = false, bool isWeekend = false}) {
final phase = _getPhaseForDate(day, lastPeriodStart, cycleLength); final phase = _getPhaseForDate(day, lastPeriodStart, cycleLength);
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
// Determine the Day of Cycle // Determine the Day of Cycle
int? doc; int? doc;
if (lastPeriodStart != null) { if (lastPeriodStart != null) {
@@ -876,14 +901,22 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
// Text style // Text style
TextStyle textStyle = GoogleFonts.outfit( TextStyle textStyle = GoogleFonts.outfit(
fontSize: (isOvulationDay || isPeriodStart) ? 18 : 14, fontSize: (isOvulationDay || isPeriodStart) ? 18 : 14,
fontWeight: (isOvulationDay || isPeriodStart) ? FontWeight.bold : FontWeight.normal, fontWeight: (isOvulationDay || isPeriodStart)
color: isSelected ? Colors.white : (isToday ? AppColors.sageGreen : (Theme.of(context).textTheme.bodyMedium?.color)), ? FontWeight.bold
: FontWeight.normal,
color: isSelected
? Colors.white
: (isToday
? AppColors.sageGreen
: (Theme.of(context).textTheme.bodyMedium?.color)),
); );
if (isOvulationDay) { if (isOvulationDay) {
textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.ovulationPhase); textStyle = textStyle.copyWith(
color: isSelected ? Colors.white : AppColors.ovulationPhase);
} else if (isPeriodStart) { } else if (isPeriodStart) {
textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.menstrualPhase); textStyle = textStyle.copyWith(
color: isSelected ? Colors.white : AppColors.menstrualPhase);
} }
return Container( return Container(

View File

@@ -12,14 +12,14 @@ import '../devotional/devotional_screen.dart';
import '../settings/appearance_screen.dart'; import '../settings/appearance_screen.dart';
import '../settings/cycle_settings_screen.dart'; import '../settings/cycle_settings_screen.dart';
import '../settings/relationship_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/cycle_history_screen.dart';
import '../settings/sharing_settings_screen.dart'; import '../settings/sharing_settings_screen.dart';
import '../settings/notification_settings_screen.dart'; import '../settings/notification_settings_screen.dart';
import '../settings/privacy_settings_screen.dart'; import '../settings/privacy_settings_screen.dart';
import '../settings/supplies_settings_screen.dart'; import '../settings/supplies_settings_screen.dart';
import '../settings/export_data_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/tip_card.dart';
import '../../widgets/cycle_ring.dart'; import '../../widgets/cycle_ring.dart';
import '../../widgets/scripture_card.dart'; import '../../widgets/scripture_card.dart';
@@ -37,7 +37,8 @@ class HomeScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final selectedIndex = ref.watch(navigationProvider); 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<Widget> tabs;
final List<BottomNavigationBarItem> navBarItems; final List<BottomNavigationBarItem> navBarItems;
@@ -50,16 +51,38 @@ class HomeScreen extends ConsumerWidget {
const LogScreen(), const LogScreen(),
const DevotionalScreen(), const DevotionalScreen(),
const WifeLearnScreen(), const WifeLearnScreen(),
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)), _SettingsTab(
onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
]; ];
navBarItems = [ navBarItems = [
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'), const BottomNavigationBarItem(
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'), icon: Icon(Icons.home_outlined),
const BottomNavigationBarItem(icon: Icon(Icons.inventory_2_outlined), activeIcon: Icon(Icons.inventory_2), label: 'Supplies'), activeIcon: Icon(Icons.home),
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'), label: 'Home'),
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'), const BottomNavigationBarItem(
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'), icon: Icon(Icons.calendar_today_outlined),
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'), 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 { } else {
tabs = [ tabs = [
@@ -68,15 +91,34 @@ class HomeScreen extends ConsumerWidget {
const DevotionalScreen(), const DevotionalScreen(),
const LogScreen(), const LogScreen(),
const WifeLearnScreen(), const WifeLearnScreen(),
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)), _SettingsTab(
onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
]; ];
navBarItems = [ navBarItems = [
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'), const BottomNavigationBarItem(
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'), icon: Icon(Icons.home_outlined),
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'), activeIcon: Icon(Icons.home),
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'), label: 'Home'),
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'), const BottomNavigationBarItem(
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Listen for changes in the cycle info to re-initialize scripture if needed // 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) { if (previousCycleInfo?.phase != newCycleInfo.phase) {
_initializeScripture(); _initializeScripture();
} }
@@ -145,8 +188,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
final translation = final translation =
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation)) ?? ref.watch(userProfileProvider.select((u) => u?.bibleTranslation)) ??
BibleTranslation.esv; BibleTranslation.esv;
final role = ref.watch(userProfileProvider.select((u) => u?.role)) ?? final role =
UserRole.wife; ref.watch(userProfileProvider.select((u) => u?.role)) ?? UserRole.wife;
final isMarried = final isMarried =
ref.watch(userProfileProvider.select((u) => u?.isMarried)) ?? false; ref.watch(userProfileProvider.select((u) => u?.isMarried)) ?? false;
final averageCycleLength = final averageCycleLength =
@@ -163,7 +206,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
final maxIndex = scriptureState.maxIndex; final maxIndex = scriptureState.maxIndex;
if (scripture == null) { if (scripture == null) {
return const Center(child: CircularProgressIndicator()); // Or some error message return const Center(
child: CircularProgressIndicator()); // Or some error message
} }
return SafeArea( return SafeArea(
@@ -181,10 +225,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
phase: phase, phase: phase,
), ),
), ),
if (phase == CyclePhase.menstrual) ...[ const SizedBox(height: 24),
const SizedBox(height: 24), const PadTrackerCard(),
const PadTrackerCard(),
],
const SizedBox(height: 32), const SizedBox(height: 32),
// Main Scripture Card with Navigation // Main Scripture Card with Navigation
Stack( Stack(
@@ -203,8 +245,9 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
left: 0, left: 0,
child: IconButton( child: IconButton(
icon: Icon(Icons.arrow_back_ios), icon: Icon(Icons.arrow_back_ios),
onPressed: () => onPressed: () => ref
ref.read(scriptureProvider.notifier).getPreviousScripture(), .read(scriptureProvider.notifier)
.getPreviousScripture(),
color: AppColors.charcoal, color: AppColors.charcoal,
), ),
), ),
@@ -212,8 +255,9 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
right: 0, right: 0,
child: IconButton( child: IconButton(
icon: Icon(Icons.arrow_forward_ios), icon: Icon(Icons.arrow_forward_ios),
onPressed: () => onPressed: () => ref
ref.read(scriptureProvider.notifier).getNextScripture(), .read(scriptureProvider.notifier)
.getNextScripture(),
color: AppColors.charcoal, color: AppColors.charcoal,
), ),
), ),
@@ -222,16 +266,17 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (maxIndex != null && maxIndex > 1) if (maxIndex != null && maxIndex > 1)
Center( Center(
child: TextButton.icon( child: TextButton.icon(
onPressed: () => ref.read(scriptureProvider.notifier).getRandomScripture(), onPressed: () =>
icon: const Icon(Icons.shuffle), ref.read(scriptureProvider.notifier).getRandomScripture(),
label: const Text('Random Verse'), icon: const Icon(Icons.shuffle),
style: TextButton.styleFrom( label: const Text('Random Verse'),
foregroundColor: Theme.of(context).colorScheme.primary, style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.primary,
),
), ),
), ),
),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'Quick Log', 'Quick Log',
@@ -243,8 +288,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
const SizedBox(height: 12), const SizedBox(height: 12),
const QuickLogButtons(), const QuickLogButtons(),
const SizedBox(height: 24), const SizedBox(height: 24),
if (role == UserRole.wife) if (role == UserRole.wife) _buildWifeTipsSection(context),
_buildWifeTipsSection(context),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
), ),
@@ -364,18 +408,18 @@ class _SettingsTab extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final name = final name =
ref.watch(userProfileProvider.select((u) => u?.name)) ?? 'Guest'; ref.watch(userProfileProvider.select((u) => u?.name)) ?? 'Guest';
final roleSymbol = final roleSymbol = ref.watch(userProfileProvider.select((u) => u?.role)) ==
ref.watch(userProfileProvider.select((u) => u?.role)) == UserRole.husband
UserRole.husband ? 'HUSBAND'
? 'HUSBAND' : null;
: null;
final relationshipStatus = ref.watch(userProfileProvider final relationshipStatus = ref.watch(userProfileProvider
.select((u) => u?.relationshipStatus.name.toUpperCase())) ?? .select((u) => u?.relationshipStatus.name.toUpperCase())) ??
'SINGLE'; 'SINGLE';
final translationLabel = final translationLabel = ref.watch(
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? userProfileProvider.select((u) => u?.bibleTranslation.label)) ??
'ESV'; 'ESV';
final isSingle = ref.watch(userProfileProvider.select((u) => u?.relationshipStatus == RelationshipStatus.single)); final isSingle = ref.watch(userProfileProvider
.select((u) => u?.relationshipStatus == RelationshipStatus.single));
return SafeArea( return SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -398,8 +442,10 @@ class _SettingsTab extends ConsumerWidget {
color: Theme.of(context).cardTheme.color, color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: color: Theme.of(context)
Theme.of(context).colorScheme.outline.withOpacity(0.05)), .colorScheme
.outline
.withOpacity(0.05)),
), ),
child: Row( child: Row(
children: [ children: [
@@ -409,8 +455,14 @@ class _SettingsTab extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.7), Theme.of(context)
Theme.of(context).colorScheme.secondary.withOpacity(0.7) .colorScheme
.primary
.withOpacity(0.7),
Theme.of(context)
.colorScheme
.secondary
.withOpacity(0.7)
], ],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@@ -459,14 +511,15 @@ class _SettingsTab extends ConsumerWidget {
const SizedBox(height: 24), const SizedBox(height: 24),
_buildSettingsGroup(context, 'Preferences', [ _buildSettingsGroup(context, 'Preferences', [
_buildSettingsTile( _buildSettingsTile(
context, context,
Icons.notifications_outlined, Icons.notifications_outlined,
'Notifications', 'Notifications',
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const NotificationSettingsScreen())); builder: (context) =>
const NotificationSettingsScreen()));
}, },
), ),
_buildSettingsTile( _buildSettingsTile(
@@ -477,7 +530,8 @@ class _SettingsTab extends ConsumerWidget {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const SuppliesSettingsScreen())); builder: (context) =>
const SuppliesSettingsScreen()));
}, },
), ),
_buildSettingsTile( _buildSettingsTile(
@@ -488,12 +542,13 @@ class _SettingsTab extends ConsumerWidget {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const RelationshipSettingsScreen())); builder: (context) =>
const RelationshipSettingsScreen()));
}, },
), ),
_buildSettingsTile( _buildSettingsTile(
context, context,
Icons.flag_outlined, Icons.flag_outlined,
'Cycle Goal', 'Cycle Goal',
onTap: () { onTap: () {
Navigator.push( Navigator.push(
@@ -521,8 +576,7 @@ class _SettingsTab extends ConsumerWidget {
'My Favorites', 'My Favorites',
onTap: () => _showFavoritesDialog(context, ref), onTap: () => _showFavoritesDialog(context, ref),
), ),
_buildSettingsTile( _buildSettingsTile(context, Icons.security, 'Privacy & Security',
context, Icons.security, 'Privacy & Security',
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -562,8 +616,7 @@ class _SettingsTab extends ConsumerWidget {
builder: (context) => CycleHistoryScreen())); builder: (context) => CycleHistoryScreen()));
}), }),
_buildSettingsTile( _buildSettingsTile(
context, Icons.download_outlined, 'Export Data', context, Icons.download_outlined, 'Export Data', onTap: () {
onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -629,7 +682,9 @@ class _SettingsTab extends ConsumerWidget {
autofocus: true, autofocus: true,
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel')),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.pop(context, controller.text), onPressed: () => Navigator.pop(context, controller.text),
child: const Text('Unlock'), child: const Text('Unlock'),
@@ -648,7 +703,8 @@ class _SettingsTab extends ConsumerWidget {
final granted = await _authenticate(context, userProfile.privacyPin!); final granted = await _authenticate(context, userProfile.privacyPin!);
if (!granted) { if (!granted) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Incorrect PIN'))); ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Incorrect PIN')));
} }
return; return;
} }
@@ -663,14 +719,16 @@ class _SettingsTab extends ConsumerWidget {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( 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( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'List your favorite comfort foods, snacks, or flowers so your husband knows what to get you!', '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), const SizedBox(height: 16),
TextField( TextField(
@@ -696,8 +754,11 @@ class _SettingsTab extends ConsumerWidget {
.where((e) => e.isNotEmpty) .where((e) => e.isNotEmpty)
.toList(); .toList();
final updatedProfile = userProfile.copyWith(favoriteFoods: favorites); final updatedProfile =
ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); userProfile.copyWith(favoriteFoods: favorites);
ref
.read(userProfileProvider.notifier)
.updateProfile(updatedProfile);
Navigator.pop(context); Navigator.pop(context);
}, },
child: const Text('Save'), child: const Text('Save'),
@@ -710,14 +771,16 @@ class _SettingsTab extends ConsumerWidget {
void _showShareDialog(BuildContext context, WidgetRef ref) { void _showShareDialog(BuildContext context, WidgetRef ref) {
// Generate a simple pairing code (in a real app, this would be stored/validated) // Generate a simple pairing code (in a real app, this would be stored/validated)
final userProfile = ref.read(userProfileProvider); final userProfile = ref.read(userProfileProvider);
final pairingCode = userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123'; final pairingCode =
userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123';
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Row( title: Row(
children: [ 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 SizedBox(width: 8),
const Text('Share with Husband'), const Text('Share with Husband'),
], ],
@@ -727,7 +790,8 @@ class _SettingsTab extends ConsumerWidget {
children: [ children: [
Text( Text(
'Share this code with your husband so he can connect to your cycle data:', '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), const SizedBox(height: 24),
Container( Container(
@@ -735,7 +799,9 @@ class _SettingsTab extends ConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1), color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12), 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( child: SelectableText(
pairingCode, pairingCode,
@@ -750,7 +816,8 @@ class _SettingsTab extends ConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'He can enter this in his app under Settings > Connect with Wife.', '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, 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../../models/user_profile.dart'; import '../../models/user_profile.dart';
import '../../models/teaching_plan.dart'; import '../../models/teaching_plan.dart';
import '../../providers/user_provider.dart'; import '../../providers/user_provider.dart';
import '../../theme/app_theme.dart'; import '../../theme/app_theme.dart';
import '../../services/bible_xml_parser.dart'; import '../../services/bible_xml_parser.dart';
import '../../services/mock_data_service.dart';
class HusbandDevotionalScreen extends ConsumerStatefulWidget { class HusbandDevotionalScreen extends ConsumerStatefulWidget {
const HusbandDevotionalScreen({super.key}); const HusbandDevotionalScreen({super.key});
@override @override
ConsumerState<HusbandDevotionalScreen> createState() => _HusbandDevotionalScreenState(); ConsumerState<HusbandDevotionalScreen> createState() =>
_HusbandDevotionalScreenState();
} }
class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScreen> { class _HusbandDevotionalScreenState
extends ConsumerState<HusbandDevotionalScreen> {
final _parser = BibleXmlParser(); final _parser = BibleXmlParser();
Map<String, String> _scriptures = {}; Map<String, String> _scriptures = {};
bool _loading = true; bool _loading = true;
@@ -34,15 +37,16 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
Future<void> _fetchScriptures() async { Future<void> _fetchScriptures() async {
final user = ref.read(userProfileProvider); final user = ref.read(userProfileProvider);
if (user == null) return; if (user == null) return;
final translation = user.bibleTranslation; final translation = user.bibleTranslation;
if (translation == _currentTranslation && _scriptures.isNotEmpty) return; if (translation == _currentTranslation && _scriptures.isNotEmpty) return;
setState(() => _loading = true); setState(() => _loading = true);
try { try {
final assetPath = 'assets/bible_xml/${translation.name.toUpperCase()}.xml'; final assetPath =
'assets/bible_xml/${translation.name.toUpperCase()}.xml';
// Define verses to fetch // Define verses to fetch
final versesToFetch = [ final versesToFetch = [
'1 Corinthians 11:3', '1 Corinthians 11:3',
@@ -53,7 +57,7 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
]; ];
final Map<String, String> results = {}; final Map<String, String> results = {};
for (final ref in versesToFetch) { for (final ref in versesToFetch) {
final text = await _parser.getVerseFromAsset(assetPath, ref); final text = await _parser.getVerseFromAsset(assetPath, ref);
results[ref] = text ?? 'Verse not found.'; results[ref] = text ?? 'Verse not found.';
@@ -74,7 +78,8 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
void _showAddTeachingDialog([TeachingPlan? existingPlan]) { void _showAddTeachingDialog([TeachingPlan? existingPlan]) {
final titleController = TextEditingController(text: existingPlan?.topic); final titleController = TextEditingController(text: existingPlan?.topic);
final scriptureController = TextEditingController(text: existingPlan?.scriptureReference); final scriptureController =
TextEditingController(text: existingPlan?.scriptureReference);
final notesController = TextEditingController(text: existingPlan?.notes); final notesController = TextEditingController(text: existingPlan?.notes);
DateTime selectedDate = existingPlan?.date ?? DateTime.now(); DateTime selectedDate = existingPlan?.date ?? DateTime.now();
@@ -125,7 +130,8 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
context: context, context: context,
initialDate: selectedDate, initialDate: selectedDate,
firstDate: DateTime.now(), firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)), lastDate:
DateTime.now().add(const Duration(days: 365)),
); );
if (picked != null) { if (picked != null) {
setState(() => selectedDate = picked); setState(() => selectedDate = picked);
@@ -145,41 +151,50 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
if (titleController.text.isEmpty) return; if (titleController.text.isEmpty) return;
final user = ref.read(userProfileProvider); final user = ref.read(userProfileProvider);
if (user == null) return; if (user == null) return;
TeachingPlan newPlan; TeachingPlan newPlan;
if (existingPlan != null) { if (existingPlan != null) {
newPlan = existingPlan.copyWith( newPlan = existingPlan.copyWith(
topic: titleController.text, topic: titleController.text,
scriptureReference: scriptureController.text, scriptureReference: scriptureController.text,
notes: notesController.text, notes: notesController.text,
date: selectedDate, date: selectedDate,
); );
} else { } else {
newPlan = TeachingPlan.create( newPlan = TeachingPlan.create(
topic: titleController.text, topic: titleController.text,
scriptureReference: scriptureController.text, scriptureReference: scriptureController.text,
notes: notesController.text, notes: notesController.text,
date: selectedDate, date: selectedDate,
); );
} }
List<TeachingPlan> updatedList = List.from(user.teachingPlans ?? []); List<TeachingPlan> updatedList =
if (existingPlan != null) { List.from(user.teachingPlans ?? []);
final index = updatedList.indexWhere((p) => p.id == existingPlan.id); if (existingPlan != null) {
if (index != -1) updatedList[index] = newPlan; final index =
} else { updatedList.indexWhere((p) => p.id == existingPlan.id);
updatedList.add(newPlan); if (index != -1) updatedList[index] = newPlan;
} } else {
updatedList.add(newPlan);
}
await ref.read(userProfileProvider.notifier).updateProfile( await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList), 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'), child: const Text('Save'),
), ),
@@ -193,10 +208,11 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
final user = ref.read(userProfileProvider); final user = ref.read(userProfileProvider);
if (user == null || user.teachingPlans == null) return; 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( await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList), user.copyWith(teachingPlans: updatedList),
); );
} }
void _toggleComplete(TeachingPlan plan) async { 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); if (p.id == plan.id) return p.copyWith(isCompleted: !p.isCompleted);
return p; return p;
}).toList(); }).toList();
await ref.read(userProfileProvider.notifier).updateProfile( await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList), user.copyWith(teachingPlans: updatedList),
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = ref.watch(userProfileProvider); final user = ref.watch(userProfileProvider);
final upcomingPlans = user?.teachingPlans ?? []; 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 // Listen for translation changes to re-fetch
ref.listen(userProfileProvider, (prev, next) { ref.listen(userProfileProvider, (prev, next) {
@@ -253,12 +269,13 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
), ),
IconButton( IconButton(
onPressed: () => _showAddTeachingDialog(), 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), const SizedBox(height: 12),
if (upcomingPlans.isEmpty) if (upcomingPlans.isEmpty)
Container( Container(
width: double.infinity, width: double.infinity,
@@ -303,39 +320,48 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
onDismissed: (_) => _deletePlan(plan), onDismissed: (_) => _deletePlan(plan),
child: Card( child: Card(
elevation: 2, elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
child: ListTile( child: ListTile(
onTap: () => _showAddTeachingDialog(plan), onTap: () => _showAddTeachingDialog(plan),
leading: IconButton( leading: IconButton(
icon: Icon( icon: Icon(
plan.isCompleted ? Icons.check_circle : Icons.circle_outlined, plan.isCompleted
color: plan.isCompleted ? Colors.green : Colors.grey ? Icons.check_circle
), : Icons.circle_outlined,
onPressed: () => _toggleComplete(plan), color: plan.isCompleted
? Colors.green
: Colors.grey),
onPressed: () => _toggleComplete(plan),
), ),
title: Text( title: Text(
plan.topic, plan.topic,
style: GoogleFonts.outfit( style: GoogleFonts.outfit(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
decoration: plan.isCompleted ? TextDecoration.lineThrough : null, decoration: plan.isCompleted
? TextDecoration.lineThrough
: null,
), ),
), ),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (plan.scriptureReference.isNotEmpty) 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) if (plan.notes.isNotEmpty)
Text( Text(
plan.notes, plan.notes,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
DateFormat.yMMMd().format(plan.date), DateFormat.yMMMd().format(plan.date),
style: TextStyle(fontSize: 11, color: Colors.grey[600]), style: TextStyle(
), fontSize: 11, color: Colors.grey[600]),
),
], ],
), ),
isThreeLine: true, 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 // Combine 1 Timothy verses
String timothyText = 'Loading...'; String timothyText = 'Loading...';
if (!_loading) { if (!_loading) {
timothyText = '${_scriptures['1 Timothy 3:4'] ?? '...'} ${_scriptures['1 Timothy 3:5'] ?? ''} ... ${_scriptures['1 Timothy 3:12'] ?? ''}'; timothyText =
// Cleanup potential double spaces or missing '${_scriptures['1 Timothy 3:4'] ?? '...'} ${_scriptures['1 Timothy 3:5'] ?? ''} ... ${_scriptures['1 Timothy 3:12'] ?? ''}';
timothyText = timothyText.replaceAll(' ', ' ').trim(); // Cleanup potential double spaces or missing
timothyText = timothyText.replaceAll(' ', ' ').trim();
} }
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -376,28 +408,31 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
const Icon(Icons.menu_book, color: Color(0xFF8B5E3C)), const Icon(Icons.menu_book, color: Color(0xFF8B5E3C)),
const SizedBox(width: 12), const SizedBox(width: 12),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Biblical Principles', 'Biblical Principles',
style: GoogleFonts.lora( style: GoogleFonts.lora(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: const Color(0xFF5D4037), color: const Color(0xFF5D4037),
), ),
), ),
Text( Text(
version, version,
style: GoogleFonts.outfit(fontSize: 12, color: const Color(0xFF8B5E3C)), style: GoogleFonts.outfit(
), fontSize: 12, color: const Color(0xFF8B5E3C)),
], ),
],
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildVerseText( _buildVerseText(
'1 Corinthians 11:3', '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.', 'Supports family structure under Christs authority.',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -409,9 +444,11 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
'Qualifications for church elders include managing their own households well.', 'Qualifications for church elders include managing their own households well.',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildVerseText( _buildVerseText(
'Titus 1:6', '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.', '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), const SizedBox(height: 4),
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: Text( child: Text(
text, text,
key: ValueKey(text), // Animate change key: ValueKey(text), // Animate change
style: GoogleFonts.lora( style: GoogleFonts.lora(
fontSize: 15, fontSize: 15,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
height: 1.4, height: 1.4,
color: const Color(0xFF3E2723), color: const Color(0xFF3E2723),
),
), ),
),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( 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 '../../models/user_profile.dart';
import '../../providers/user_provider.dart'; import '../../providers/user_provider.dart';
import '../../services/mock_data_service.dart'; import '../../services/mock_data_service.dart';
import '../../providers/cycle_provider.dart'; // Ensure cycleEntriesProvider is available import 'husband_appearance_screen.dart';
import '../settings/appearance_screen.dart';
class HusbandSettingsScreen extends ConsumerWidget { class HusbandSettingsScreen extends ConsumerWidget {
const HusbandSettingsScreen({super.key}); const HusbandSettingsScreen({super.key});
@@ -72,20 +71,22 @@ class HusbandSettingsScreen extends ConsumerWidget {
final mockWife = mockService.generateMockWifeProfile(); final mockWife = mockService.generateMockWifeProfile();
final currentProfile = ref.read(userProfileProvider); final currentProfile = ref.read(userProfileProvider);
if (currentProfile != null) { if (currentProfile != null) {
final updatedProfile = currentProfile.copyWith( final updatedProfile = currentProfile.copyWith(
partnerName: mockWife.name, partnerName: mockWife.name,
averageCycleLength: mockWife.averageCycleLength, averageCycleLength: mockWife.averageCycleLength,
averagePeriodLength: mockWife.averagePeriodLength, averagePeriodLength: mockWife.averagePeriodLength,
lastPeriodStartDate: mockWife.lastPeriodStartDate, lastPeriodStartDate: mockWife.lastPeriodStartDate,
favoriteFoods: mockWife.favoriteFoods, favoriteFoods: mockWife.favoriteFoods,
); );
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); await ref
.read(userProfileProvider.notifier)
.updateProfile(updatedProfile);
} }
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Demo data loaded')), const SnackBar(content: Text('Demo data loaded')),
); );
} }
} }
} }
@@ -112,23 +113,26 @@ class HusbandSettingsScreen extends ConsumerWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
...BibleTranslation.values.map((translation) => ListTile( ...BibleTranslation.values.map((translation) => ListTile(
title: Text( title: Text(
translation.label, translation.label,
style: GoogleFonts.outfit(fontWeight: FontWeight.w500), style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
), ),
trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation trailing: ref.watch(userProfileProvider)?.bibleTranslation ==
? const Icon(Icons.check, color: AppColors.sageGreen) translation
: null, ? const Icon(Icons.check, color: AppColors.sageGreen)
onTap: () async { : null,
final profile = ref.read(userProfileProvider); onTap: () async {
if (profile != null) { final profile = ref.read(userProfileProvider);
await ref.read(userProfileProvider.notifier).updateProfile( if (profile != null) {
profile.copyWith(bibleTranslation: translation), await ref
); .read(userProfileProvider.notifier)
} .updateProfile(
if (context.mounted) Navigator.pop(context); profile.copyWith(bibleTranslation: translation),
}, );
)), }
if (context.mounted) Navigator.pop(context);
},
)),
], ],
), ),
), ),
@@ -138,104 +142,112 @@ class HusbandSettingsScreen extends ConsumerWidget {
void _showConnectDialog(BuildContext context, WidgetRef ref) { void _showConnectDialog(BuildContext context, WidgetRef ref) {
final codeController = TextEditingController(); final codeController = TextEditingController();
bool shareDevotional = true; bool shareDevotional = true;
showDialog( showDialog(
context: context, context: context,
builder: (context) => StatefulBuilder( builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog( builder: (context, setState) => AlertDialog(
title: Row( title: Row(
children: [ children: [
const Icon(Icons.link, color: AppColors.navyBlue), const Icon(Icons.link, color: AppColors.navyBlue),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text('Connect with Wife'), const Text('Connect with Wife'),
], ],
), ),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'Enter the pairing code from your wife\'s app:', 'Enter the pairing code from your wife\'s app:',
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), 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),
), TextField(
const SizedBox(height: 16), controller: codeController,
Text( decoration: const InputDecoration(
'Your wife can find this code in her Settings under "Share with Husband".', hintText: 'e.g., ABC123',
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), border: OutlineInputBorder(),
), ),
const SizedBox(height: 24), textCapitalization: TextCapitalization.characters,
Row( ),
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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( SizedBox(
height: 24, height: 24,
width: 24, width: 24,
child: Checkbox( child: Checkbox(
value: shareDevotional, value: shareDevotional,
onChanged: (val) => setState(() => shareDevotional = val ?? true), onChanged: (val) =>
activeColor: AppColors.navyBlue, setState(() => shareDevotional = val ?? true),
), activeColor: AppColors.navyBlue,
), ),
const SizedBox(width: 12), ),
Expanded( const SizedBox(width: 12),
child: Column( Expanded(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'Share Devotional Plans', Text(
style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.charcoal), 'Share Devotional Plans',
), style: GoogleFonts.outfit(
Text( fontWeight: FontWeight.bold,
'Allow her to see the teaching plans you create.', fontSize: 14,
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), 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( actions: [
onPressed: () async { TextButton(
final code = codeController.text.trim(); onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
Navigator.pop(context); ),
ElevatedButton(
// Update preference onPressed: () async {
final user = ref.read(userProfileProvider); final code = codeController.text.trim();
if (user != null) {
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(isDataShared: shareDevotional)
);
}
ScaffoldMessenger.of(context).showSnackBar( Navigator.pop(context);
const SnackBar(
content: Text('Settings updated & Connected!'), // Update preference
backgroundColor: AppColors.sageGreen, final user = ref.read(userProfileProvider);
), if (user != null) {
); await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(isDataShared: shareDevotional));
if (code.isNotEmpty) { }
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Settings updated & Connected!'),
backgroundColor: AppColors.sageGreen,
),
);
if (code.isNotEmpty) {
// Load demo data as simulation // Load demo data as simulation
final mockService = MockDataService(); final mockService = MockDataService();
final entries = mockService.generateMockCycleEntries(); final entries = mockService.generateMockCycleEntries();
for (var entry in entries) { for (var entry in entries) {
await ref.read(cycleEntriesProvider.notifier).addEntry(entry); await ref
.read(cycleEntriesProvider.notifier)
.addEntry(entry);
} }
final mockWife = mockService.generateMockWifeProfile(); final mockWife = mockService.generateMockWifeProfile();
final currentProfile = ref.read(userProfileProvider); final currentProfile = ref.read(userProfileProvider);
@@ -248,18 +260,20 @@ class HusbandSettingsScreen extends ConsumerWidget {
lastPeriodStartDate: mockWife.lastPeriodStartDate, lastPeriodStartDate: mockWife.lastPeriodStartDate,
favoriteFoods: mockWife.favoriteFoods, favoriteFoods: mockWife.favoriteFoods,
); );
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); await ref
.read(userProfileProvider.notifier)
.updateProfile(updatedProfile);
} }
} }
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue, backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white, 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) { Widget build(BuildContext context, WidgetRef ref) {
// Theme aware colors // Theme aware colors
final isDark = Theme.of(context).brightness == Brightness.dark; 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; final textColor = Theme.of(context).textTheme.bodyLarge?.color;
return SafeArea( return SafeArea(
@@ -282,7 +297,8 @@ class HusbandSettingsScreen extends ConsumerWidget {
style: GoogleFonts.outfit( style: GoogleFonts.outfit(
fontSize: 28, fontSize: 28,
fontWeight: FontWeight.w600, 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), const SizedBox(height: 24),
@@ -297,7 +313,8 @@ class HusbandSettingsScreen extends ConsumerWidget {
leading: Icon(Icons.notifications_outlined, leading: Icon(Icons.notifications_outlined,
color: Theme.of(context).colorScheme.primary), color: Theme.of(context).colorScheme.primary),
title: Text('Notifications', 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) {}), trailing: Switch(value: true, onChanged: (val) {}),
), ),
const Divider(height: 1), const Divider(height: 1),
@@ -305,8 +322,10 @@ class HusbandSettingsScreen extends ConsumerWidget {
leading: Icon(Icons.link, leading: Icon(Icons.link,
color: Theme.of(context).colorScheme.primary), color: Theme.of(context).colorScheme.primary),
title: Text('Connect with Wife', title: Text('Connect with Wife',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)), style: GoogleFonts.outfit(
trailing: Icon(Icons.chevron_right, color: Theme.of(context).disabledColor), fontWeight: FontWeight.w500, color: textColor)),
trailing: Icon(Icons.chevron_right,
color: Theme.of(context).disabledColor),
onTap: () => _showConnectDialog(context, ref), onTap: () => _showConnectDialog(context, ref),
), ),
const Divider(height: 1), const Divider(height: 1),
@@ -314,18 +333,24 @@ class HusbandSettingsScreen extends ConsumerWidget {
leading: Icon(Icons.menu_book_outlined, leading: Icon(Icons.menu_book_outlined,
color: Theme.of(context).colorScheme.primary), color: Theme.of(context).colorScheme.primary),
title: Text('Bible Translation', title: Text('Bible Translation',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)), style: GoogleFonts.outfit(
fontWeight: FontWeight.w500, color: textColor)),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? 'ESV', ref.watch(userProfileProvider
.select((u) => u?.bibleTranslation.label)) ??
'ESV',
style: GoogleFonts.outfit( style: GoogleFonts.outfit(
fontSize: 14, 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), onTap: () => _showTranslationPicker(context, ref),
@@ -335,13 +360,15 @@ class HusbandSettingsScreen extends ConsumerWidget {
leading: Icon(Icons.palette_outlined, leading: Icon(Icons.palette_outlined,
color: Theme.of(context).colorScheme.primary), color: Theme.of(context).colorScheme.primary),
title: Text('Appearance', title: Text('Appearance',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)), style: GoogleFonts.outfit(
trailing: Icon(Icons.chevron_right, color: Theme.of(context).disabledColor), fontWeight: FontWeight.w500, color: textColor)),
trailing: Icon(Icons.chevron_right,
color: Theme.of(context).disabledColor),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( 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:hive_flutter/hive_flutter.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../../theme/app_theme.dart'; import '../../theme/app_theme.dart';
import 'package:christian_period_tracker/models/user_profile.dart'; import '../../models/user_profile.dart';
import 'package:christian_period_tracker/models/cycle_entry.dart'; import '../../models/cycle_entry.dart';
import '../home/home_screen.dart'; import '../home/home_screen.dart';
import '../husband/husband_home_screen.dart'; import '../husband/husband_home_screen.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/user_provider.dart'; import '../../providers/user_provider.dart';
import '../../services/notification_service.dart';
class OnboardingScreen extends ConsumerStatefulWidget { class OnboardingScreen extends ConsumerStatefulWidget {
const OnboardingScreen({super.key}); const OnboardingScreen({super.key});
@@ -35,6 +36,10 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
int _maxCycleLength = 35; int _maxCycleLength = 35;
bool _isPadTrackingEnabled = false; bool _isPadTrackingEnabled = false;
// Connection options
bool _useExampleData = false;
bool _skipPartnerConnection = false;
@override @override
void dispose() { void dispose() {
_pageController.dispose(); _pageController.dispose();
@@ -45,14 +50,15 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
if (_isNavigating) return; if (_isNavigating) return;
_isNavigating = true; _isNavigating = true;
// Husband Flow: Role (0) -> Name (1) -> Finish // Husband Flow: Role (0) -> Name (1) -> Connect (2) -> Finish
// Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4) // Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4) -> [Connect (5) if married]
int nextPage = _currentPage + 1; int nextPage = _currentPage + 1;
// Logic for skipping pages // Logic for skipping pages
if (_role == UserRole.husband) { if (_role == UserRole.husband) {
if (_currentPage == 1) { if (_currentPage == 2) {
// Finish after connect page
await _completeOnboarding(); await _completeOnboarding();
return; return;
} }
@@ -63,10 +69,21 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
// Skip fertility goal (page 3) if not married // Skip fertility goal (page 3) if not married
nextPage = 4; 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) { final maxPages = _role == UserRole.husband ? 2 : 5;
// Max pages if (nextPage <= maxPages) {
await _pageController.animateToPage( await _pageController.animateToPage(
nextPage, nextPage,
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
@@ -124,18 +141,24 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
? _fertilityGoal ? _fertilityGoal
: null, : null,
averageCycleLength: _averageCycleLength, averageCycleLength: _averageCycleLength,
minCycleLength: _minCycleLength,
maxCycleLength: _maxCycleLength,
lastPeriodStartDate: _lastPeriodStart, lastPeriodStartDate: _lastPeriodStart,
isIrregularCycle: _isIrregularCycle, isIrregularCycle: _isIrregularCycle,
isPadTrackingEnabled: _isPadTrackingEnabled,
hasCompletedOnboarding: true, hasCompletedOnboarding: true,
useExampleData: _useExampleData,
createdAt: DateTime.now(), createdAt: DateTime.now(),
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
await ref.read(userProfileProvider.notifier).updateProfile(userProfile); 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) { if (mounted) {
// Navigate to appropriate home screen // Navigate to appropriate home screen
if (_role == UserRole.husband) { if (_role == UserRole.husband) {
@@ -174,7 +197,11 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: SmoothPageIndicator( child: SmoothPageIndicator(
controller: _pageController, controller: _pageController,
count: isHusband ? 2 : 5, count: isHusband
? 3
: (_relationshipStatus == RelationshipStatus.married
? 6
: 5),
effect: WormEffect( effect: WormEffect(
dotHeight: 8, dotHeight: 8,
dotWidth: 8, dotWidth: 8,
@@ -190,17 +217,22 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
Expanded( Expanded(
child: PageView( child: PageView(
controller: _pageController, controller: _pageController,
physics: physics: const NeverScrollableScrollPhysics(), // Disable swipe
const NeverScrollableScrollPhysics(), // Disable swipe
onPageChanged: (index) { onPageChanged: (index) {
setState(() => _currentPage = index); setState(() => _currentPage = index);
}, },
children: [ children: [
_buildRolePage(), // Page 0 _buildRolePage(), // Page 0
_buildNamePage(), // Page 1 _buildNamePage(), // Page 1
_buildRelationshipPage(), // Page 2 (Wife only) if (_role == UserRole.husband)
_buildFertilityGoalPage(), // Page 3 (Wife married only) _buildHusbandConnectPage() // Page 2 (Husband only)
_buildCyclePage(), // Page 4 (Wife 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, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [AppColors.blushPink, AppColors.rose.withOpacity(0.7)],
AppColors.blushPink,
AppColors.rose.withOpacity(0.7)
],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
@@ -304,9 +333,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color:
? activeColor isSelected ? activeColor : theme.colorScheme.surfaceVariant,
: theme.colorScheme.surfaceVariant,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
@@ -340,8 +368,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
], ],
), ),
), ),
if (isSelected) if (isSelected) Icon(Icons.check_circle, color: activeColor),
Icon(Icons.check_circle, color: activeColor),
], ],
), ),
), ),
@@ -351,8 +378,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
Widget _buildNamePage() { Widget _buildNamePage() {
final theme = Theme.of(context); final theme = Theme.of(context);
final isHusband = _role == UserRole.husband; final isHusband = _role == UserRole.husband;
final activeColor = final activeColor = isHusband ? AppColors.navyBlue : AppColors.sageGreen;
isHusband ? AppColors.navyBlue : AppColors.sageGreen;
return Padding( return Padding(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(32),
@@ -413,9 +439,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
child: SizedBox( child: SizedBox(
height: 54, height: 54,
child: ElevatedButton( child: ElevatedButton(
onPressed: (_name.isNotEmpty && !_isNavigating) onPressed:
? _nextPage (_name.isNotEmpty && !_isNavigating) ? _nextPage : null,
: null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: activeColor, backgroundColor: activeColor,
), ),
@@ -557,11 +582,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface)), color: theme.colorScheme.onSurface)),
const SizedBox(height: 32), const SizedBox(height: 32),
_buildGoalOption( _buildGoalOption(FertilityGoal.tryingToConceive, 'Trying to Conceive',
FertilityGoal.tryingToConceive, 'Track fertile days', Icons.child_care_outlined),
'Trying to Conceive',
'Track fertile days',
Icons.child_care_outlined),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildGoalOption( _buildGoalOption(
FertilityGoal.tryingToAvoid, FertilityGoal.tryingToAvoid,
@@ -692,8 +714,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
), ),
Text('$_averageCycleLength days', Text('$_averageCycleLength days',
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
color: AppColors.sageGreen)),
], ],
), ),
@@ -720,12 +741,14 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
children: [ children: [
Expanded( Expanded(
child: RangeSlider( child: RangeSlider(
values: RangeValues(_minCycleLength.toDouble(), _maxCycleLength.toDouble()), values: RangeValues(
_minCycleLength.toDouble(), _maxCycleLength.toDouble()),
min: 21, min: 21,
max: 45, max: 45,
divisions: 24, divisions: 24,
activeColor: AppColors.sageGreen, activeColor: AppColors.sageGreen,
labels: RangeLabels('$_minCycleLength days', '$_maxCycleLength days'), labels: RangeLabels(
'$_minCycleLength days', '$_maxCycleLength days'),
onChanged: (values) { onChanged: (values) {
setState(() { setState(() {
_minCycleLength = values.start.round(); _minCycleLength = values.start.round();
@@ -739,8 +762,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
Center( Center(
child: Text('$_minCycleLength - $_maxCycleLength days', child: Text('$_minCycleLength - $_maxCycleLength days',
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
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, requestBadgePermission: true,
requestSoundPermission: true, requestSoundPermission: true,
); );
// Linux initialization (optional, but good for completeness) // Linux initialization (optional, but good for completeness)
final LinuxInitializationSettings initializationSettingsLinux = final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification'); LinuxInitializationSettings(defaultActionName: 'Open notification');
final InitializationSettings initializationSettings = InitializationSettings( final InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid, android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin, iOS: initializationSettingsDarwin,
macOS: initializationSettingsDarwin, macOS: initializationSettingsDarwin,
@@ -63,10 +64,10 @@ class NotificationService {
required DateTime scheduledDate, required DateTime scheduledDate,
}) async { }) async {
if (kIsWeb) { if (kIsWeb) {
// Web platform limitation: Background scheduling is complex. // Web platform limitation: Background scheduling is complex.
// For this demo/web preview, we'll just log it or rely on the UI confirmation. // For this demo/web preview, we'll just log it or rely on the UI confirmation.
print('Web Notification Scheduled: $title - $body at $scheduledDate'); print('Web Notification Scheduled: $title - $body at $scheduledDate');
return; return;
} }
await flutterLocalNotificationsPlugin.zonedSchedule( await flutterLocalNotificationsPlugin.zonedSchedule(
@@ -92,30 +93,72 @@ class NotificationService {
// New method for specific notification types // New method for specific notification types
Future<void> showLocalNotification({ Future<void> showLocalNotification({
required int id, required int id,
required String title, required String title,
required String body, required String body,
String? channelId, String? channelId,
String? channelName, String? channelName,
}) async { }) async {
if (kIsWeb) { if (kIsWeb) {
print('Web Local Notification: $title - $body'); print('Web Local Notification: $title - $body');
return; return;
} }
const AndroidNotificationDetails androidNotificationDetails = const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails( AndroidNotificationDetails('tracker_general', 'General Notifications',
'tracker_general', 'General Notifications',
channelDescription: 'General app notifications', channelDescription: 'General app notifications',
importance: Importance.max, importance: Importance.max,
priority: Priority.high, priority: Priority.high,
ticker: 'ticker'); ticker: 'ticker');
const NotificationDetails notificationDetails = const NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails); NotificationDetails(android: androidNotificationDetails);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin
id, title, body, notificationDetails, .show(id, title, body, notificationDetails, payload: 'item x');
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 { Future<void> cancelNotification(int id) async {

View File

@@ -17,6 +17,9 @@ class PadTrackerCard extends ConsumerStatefulWidget {
class _PadTrackerCardState extends ConsumerState<PadTrackerCard> { class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
Timer? _timer; Timer? _timer;
String _timeDisplay = ''; String _timeDisplay = '';
double _progress = 0.0;
Color _statusColor = AppColors.menstrualPhase;
bool _isCountDown = true; // Toggle state
@override @override
void initState() { void initState() {
@@ -40,35 +43,92 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
void _updateTime() { void _updateTime() {
final user = ref.read(userProfileProvider); final user = ref.read(userProfileProvider);
if (user?.lastPadChangeTime == null) { if (user?.lastPadChangeTime == null) {
if (mounted) setState(() => _timeDisplay = 'Tap to start'); if (mounted) {
setState(() {
_timeDisplay = 'Tap to start';
_progress = 0;
_statusColor = AppColors.menstrualPhase;
});
}
return; return;
} }
final now = DateTime.now(); final now = DateTime.now();
final difference = now.difference(user!.lastPadChangeTime!); final difference = now.difference(user!.lastPadChangeTime!);
// We want to show time SINCE change (duration worn) // Estimate max duration based on flow
final hours = difference.inHours; // None/Precautionary: 8h, Spotting: 8h, Light: 6h, Medium: 4h, Heavy: 3h
final minutes = difference.inMinutes.remainder(60); final flowIntensity = user.typicalFlowIntensity ?? 2; // Default to light
final seconds = difference.inSeconds.remainder(60); 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 = ''; 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) { if (mounted) {
setState(() { setState(() {
_timeDisplay = text; _timeDisplay = text;
_progress = _isCountDown && text.contains('Overdue') ? 1.0 : _progress;
_statusColor = newColor;
}); });
} }
} }
@@ -76,10 +136,8 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = ref.watch(userProfileProvider); final user = ref.watch(userProfileProvider);
if (user == null || !user.isPadTrackingEnabled) return const SizedBox.shrink(); 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
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
@@ -91,53 +149,86 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.menstrualPhase.withOpacity(0.1), color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)), border: Border.all(color: _statusColor.withOpacity(0.3)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: AppColors.menstrualPhase.withOpacity(0.05), color: _statusColor.withOpacity(0.1),
blurRadius: 8, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 4),
), ),
], ],
), ),
child: Row( child: Column(
children: [ children: [
Container( Row(
padding: const EdgeInsets.all(10), children: [
decoration: BoxDecoration( Container(
color: AppColors.menstrualPhase.withOpacity(0.2), padding: const EdgeInsets.all(10),
shape: BoxShape.circle, decoration: BoxDecoration(
), color: _statusColor.withOpacity(0.1),
child: const Icon(Icons.timer_outlined, color: AppColors.menstrualPhase, size: 24), 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), const SizedBox(height: 12),
Expanded( ClipRRect(
child: Column( borderRadius: BorderRadius.circular(4),
crossAxisAlignment: CrossAxisAlignment.start, child: LinearProgressIndicator(
children: [ value: _progress,
Text( backgroundColor: _statusColor.withOpacity(0.1),
'Pad Tracker', valueColor: AlwaysStoppedAnimation<Color>(_statusColor),
style: GoogleFonts.outfit( minHeight: 6,
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 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 '../providers/user_provider.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
import '../providers/navigation_provider.dart'; import '../providers/navigation_provider.dart';
import '../models/user_profile.dart';
import 'quick_log_dialog.dart'; import 'quick_log_dialog.dart';
class QuickLogButtons extends ConsumerWidget { class QuickLogButtons extends ConsumerWidget {
@@ -63,6 +64,13 @@ class QuickLogButtons extends ConsumerWidget {
color: AppColors.lutealPhase, color: AppColors.lutealPhase,
onTap: () => _showQuickLogDialog(context, 'pads'), 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( decoration: BoxDecoration(
color: color.withOpacity(isDark ? 0.2 : 0.15), color: color.withOpacity(isDark ? 0.2 : 0.15),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: border: isDark ? Border.all(color: color.withOpacity(0.3)) : null,
isDark ? Border.all(color: color.withOpacity(0.3)) : null,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@@ -23,7 +23,7 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
FlowIntensity? _flowIntensity; FlowIntensity? _flowIntensity;
MoodLevel? _mood; MoodLevel? _mood;
int? _energyLevel; int? _energyLevel;
// Symptoms & Cravings // Symptoms & Cravings
final Map<String, bool> _symptoms = { final Map<String, bool> _symptoms = {
'Headache': false, 'Headache': false,
@@ -37,7 +37,7 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
'Insomnia': false, 'Insomnia': false,
'Cramps': false, 'Cramps': false,
}; };
final TextEditingController _cravingController = TextEditingController(); final TextEditingController _cravingController = TextEditingController();
List<String> _cravings = []; List<String> _cravings = [];
List<String> _recentCravings = []; List<String> _recentCravings = [];
@@ -102,6 +102,8 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
return _buildSymptomsLog(); return _buildSymptomsLog();
case 'cravings': case 'cravings':
return _buildCravingsLog(); return _buildCravingsLog();
case 'prayer':
return _buildPrayerLog();
default: default:
return const Text('Invalid log type.'); return const Text('Invalid log type.');
} }
@@ -137,7 +139,7 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
} }
Widget _buildCravingsLog() { Widget _buildCravingsLog() {
return Container( return Container(
width: double.maxFinite, width: double.maxFinite,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -152,37 +154,44 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
), ),
onSubmitted: (value) { onSubmitted: (value) {
if (value.isNotEmpty) { if (value.isNotEmpty) {
setState(() { setState(() {
_cravings.add(value.trim()); _cravings.add(value.trim());
_cravingController.clear(); _cravingController.clear();
}); });
} }
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Wrap( Wrap(
spacing: 8, spacing: 8,
children: _cravings.map((c) => Chip( children: _cravings
label: Text(c), .map((c) => Chip(
onDeleted: () { label: Text(c),
setState(() => _cravings.remove(c)); onDeleted: () {
}, setState(() => _cravings.remove(c));
)).toList(), },
))
.toList(),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (_recentCravings.isNotEmpty) ...[ 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), const SizedBox(height: 8),
Wrap( Wrap(
spacing: 8, spacing: 8,
children: _recentCravings.take(5).map((c) => ActionChip( children: _recentCravings
label: Text(c), .take(5)
onPressed: () { .map((c) => ActionChip(
if (!_cravings.contains(c)) { label: Text(c),
setState(() => _cravings.add(c)); onPressed: () {
} if (!_cravings.contains(c)) {
}, setState(() => _cravings.add(c));
)).toList(), }
},
))
.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 { Future<void> _saveLog() async {
// Handle text input for cravings if user didn't hit enter // Handle text input for cravings if user didn't hit enter
if (widget.logType == 'cravings' && _cravingController.text.isNotEmpty) { if (widget.logType == 'cravings' && _cravingController.text.isNotEmpty) {
_cravings.add(_cravingController.text.trim()); _cravings.add(_cravingController.text.trim());
} }
final cycleNotifier = ref.read(cycleEntriesProvider.notifier); final cycleNotifier = ref.read(cycleEntriesProvider.notifier);
@@ -288,7 +317,11 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
final entries = ref.read(cycleEntriesProvider); final entries = ref.read(cycleEntriesProvider);
final entry = entries.firstWhere( final entry = entries.firstWhere(
(e) => DateUtils.isSameDay(e.date, today), (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; CycleEntry updatedEntry = entry;
@@ -308,28 +341,61 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
break; break;
case 'symptoms': case 'symptoms':
updatedEntry = entry.copyWith( updatedEntry = entry.copyWith(
hasHeadache: _symptoms['Headache'], hasHeadache: _symptoms['Headache'],
hasBloating: _symptoms['Bloating'], hasBloating: _symptoms['Bloating'],
hasBreastTenderness: _symptoms['Breast Tenderness'], hasBreastTenderness: _symptoms['Breast Tenderness'],
hasFatigue: _symptoms['Fatigue'], hasFatigue: _symptoms['Fatigue'],
hasAcne: _symptoms['Acne'], hasAcne: _symptoms['Acne'],
hasLowerBackPain: _symptoms['Back Pain'], hasLowerBackPain: _symptoms['Back Pain'],
hasConstipation: _symptoms['Constipation'], hasConstipation: _symptoms['Constipation'],
hasDiarrhea: _symptoms['Diarrhea'], hasDiarrhea: _symptoms['Diarrhea'],
hasInsomnia: _symptoms['Insomnia'], hasInsomnia: _symptoms['Insomnia'],
crampIntensity: _symptoms['Cramps'] == true ? 2 : 0, // Default to mild cramps if just toggled 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; break;
case 'cravings': case 'cravings':
final currentCravings = entry.cravings ?? []; final currentCravings = entry.cravings ?? [];
final newCravings = {...currentCravings, ..._cravings}.toList(); final newCravings = {...currentCravings, ..._cravings}.toList();
updatedEntry = entry.copyWith(cravings: newCravings); updatedEntry = entry.copyWith(cravings: newCravings);
// Update History // Update History
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final history = prefs.getStringList('recent_cravings') ?? []; final history = prefs.getStringList('recent_cravings') ?? [];
final updatedHistory = {..._cravings, ...history}.take(20).toList(); final updatedHistory = {..._cravings, ...history}.take(20).toList();
await prefs.setStringList('recent_cravings', updatedHistory); 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; break;
default: default:
// pads handled separately // pads handled separately