Implement Notifications and Pad Tracking Enhancements
This commit is contained in:
@@ -7,10 +7,11 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "com.faithapps.christian_period_tracker"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
compileSdk = 36
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
@@ -24,7 +25,7 @@ android {
|
||||
applicationId = "com.faithapps.christian_period_tracker"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
minSdk = 26
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
@@ -39,6 +40,17 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force 'androidx.core:core:1.13.1'
|
||||
force 'androidx.core:core-ktx:1.13.1'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
<<<<<<< HEAD
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
|
||||
=======
|
||||
#Fri Dec 19 21:26:00 CST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
>>>>>>> 6742220 (Your commit message here)
|
||||
|
||||
@@ -18,7 +18,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.1.0" apply false
|
||||
id "com.android.application" version "8.6.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ enum MoodLevel {
|
||||
/// Flow intensity for period days
|
||||
@HiveType(typeId: 4)
|
||||
enum FlowIntensity {
|
||||
@HiveField(4)
|
||||
none, // No flow / Precautionary
|
||||
|
||||
@HiveField(0)
|
||||
spotting,
|
||||
|
||||
@@ -166,7 +169,8 @@ class CycleEntry extends HiveObject {
|
||||
String? husbandNotes; // Separate notes for husband
|
||||
|
||||
@HiveField(29)
|
||||
bool? intimacyProtected; // null = no selection, true = protected, false = unprotected
|
||||
bool?
|
||||
intimacyProtected; // null = no selection, true = protected, false = unprotected
|
||||
|
||||
@HiveField(30, defaultValue: false)
|
||||
bool usedPantyliner;
|
||||
@@ -174,6 +178,9 @@ class CycleEntry extends HiveObject {
|
||||
@HiveField(31, defaultValue: 0)
|
||||
int pantylinerCount;
|
||||
|
||||
@HiveField(32)
|
||||
String? prayerRequest;
|
||||
|
||||
CycleEntry({
|
||||
required this.id,
|
||||
required this.date,
|
||||
@@ -207,6 +214,7 @@ class CycleEntry extends HiveObject {
|
||||
this.husbandNotes,
|
||||
this.usedPantyliner = false,
|
||||
this.pantylinerCount = 0,
|
||||
this.prayerRequest,
|
||||
});
|
||||
|
||||
List<bool> get _symptomsList => [
|
||||
@@ -271,6 +279,7 @@ class CycleEntry extends HiveObject {
|
||||
String? husbandNotes,
|
||||
bool? usedPantyliner,
|
||||
int? pantylinerCount,
|
||||
String? prayerRequest,
|
||||
}) {
|
||||
return CycleEntry(
|
||||
id: id ?? this.id,
|
||||
@@ -292,7 +301,8 @@ class CycleEntry extends HiveObject {
|
||||
hasInsomnia: hasInsomnia ?? this.hasInsomnia,
|
||||
basalBodyTemperature: basalBodyTemperature ?? this.basalBodyTemperature,
|
||||
cervicalMucus: cervicalMucus ?? this.cervicalMucus,
|
||||
ovulationTestPositive: ovulationTestPositive ?? this.ovulationTestPositive,
|
||||
ovulationTestPositive:
|
||||
ovulationTestPositive ?? this.ovulationTestPositive,
|
||||
notes: notes ?? this.notes,
|
||||
cravings: cravings ?? this.cravings,
|
||||
sleepHours: sleepHours ?? this.sleepHours,
|
||||
@@ -305,6 +315,7 @@ class CycleEntry extends HiveObject {
|
||||
husbandNotes: husbandNotes ?? this.husbandNotes,
|
||||
usedPantyliner: usedPantyliner ?? this.usedPantyliner,
|
||||
pantylinerCount: pantylinerCount ?? this.pantylinerCount,
|
||||
prayerRequest: prayerRequest ?? this.prayerRequest,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -345,6 +356,8 @@ extension MoodLevelExtension on MoodLevel {
|
||||
extension FlowIntensityExtension on FlowIntensity {
|
||||
String get label {
|
||||
switch (this) {
|
||||
case FlowIntensity.none:
|
||||
return 'No Flow';
|
||||
case FlowIntensity.spotting:
|
||||
return 'Spotting';
|
||||
case FlowIntensity.light:
|
||||
@@ -410,11 +423,7 @@ extension CyclePhaseExtension on CyclePhase {
|
||||
case CyclePhase.ovulation:
|
||||
return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose];
|
||||
case CyclePhase.luteal:
|
||||
return [
|
||||
AppColors.lutealPhase,
|
||||
AppColors.lavender,
|
||||
AppColors.blushPink
|
||||
];
|
||||
return [AppColors.lutealPhase, AppColors.lavender, AppColors.blushPink];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,13 +49,14 @@ class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
|
||||
husbandNotes: fields[28] as String?,
|
||||
usedPantyliner: fields[30] == null ? false : fields[30] as bool,
|
||||
pantylinerCount: fields[31] == null ? 0 : fields[31] as int,
|
||||
prayerRequest: fields[32] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CycleEntry obj) {
|
||||
writer
|
||||
..writeByte(32)
|
||||
..writeByte(33)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -119,7 +120,9 @@ class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
|
||||
..writeByte(30)
|
||||
..write(obj.usedPantyliner)
|
||||
..writeByte(31)
|
||||
..write(obj.pantylinerCount);
|
||||
..write(obj.pantylinerCount)
|
||||
..writeByte(32)
|
||||
..write(obj.prayerRequest);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -194,6 +197,8 @@ class FlowIntensityAdapter extends TypeAdapter<FlowIntensity> {
|
||||
@override
|
||||
FlowIntensity read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 4:
|
||||
return FlowIntensity.none;
|
||||
case 0:
|
||||
return FlowIntensity.spotting;
|
||||
case 1:
|
||||
@@ -203,13 +208,16 @@ class FlowIntensityAdapter extends TypeAdapter<FlowIntensity> {
|
||||
case 3:
|
||||
return FlowIntensity.heavy;
|
||||
default:
|
||||
return FlowIntensity.spotting;
|
||||
return FlowIntensity.none;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, FlowIntensity obj) {
|
||||
switch (obj) {
|
||||
case FlowIntensity.none:
|
||||
writer.writeByte(4);
|
||||
break;
|
||||
case FlowIntensity.spotting:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
|
||||
@@ -302,6 +302,17 @@ class UserProfile extends HiveObject {
|
||||
@HiveField(53)
|
||||
List<TeachingPlan>? teachingPlans;
|
||||
|
||||
// Husband-specific theme settings
|
||||
@HiveField(54, defaultValue: AppThemeMode.system)
|
||||
AppThemeMode husbandThemeMode;
|
||||
|
||||
@HiveField(55, defaultValue: '0xFF1A3A5C')
|
||||
String husbandAccentColor;
|
||||
|
||||
// Whether to use example/demo data (for husband not connected to wife)
|
||||
@HiveField(56, defaultValue: false)
|
||||
bool useExampleData;
|
||||
|
||||
UserProfile({
|
||||
required this.id,
|
||||
required this.name,
|
||||
@@ -356,6 +367,9 @@ class UserProfile extends HiveObject {
|
||||
this.isCalendarProtected = false,
|
||||
this.isSuppliesProtected = false,
|
||||
this.teachingPlans,
|
||||
this.husbandThemeMode = AppThemeMode.system,
|
||||
this.husbandAccentColor = '0xFF1A3A5C',
|
||||
this.useExampleData = false,
|
||||
});
|
||||
|
||||
/// Check if user is married
|
||||
@@ -435,6 +449,9 @@ class UserProfile extends HiveObject {
|
||||
bool? isCalendarProtected,
|
||||
bool? isSuppliesProtected,
|
||||
List<TeachingPlan>? teachingPlans,
|
||||
AppThemeMode? husbandThemeMode,
|
||||
String? husbandAccentColor,
|
||||
bool? useExampleData,
|
||||
}) {
|
||||
return UserProfile(
|
||||
id: id ?? this.id,
|
||||
@@ -470,8 +487,10 @@ class UserProfile extends HiveObject {
|
||||
padBrand: padBrand ?? this.padBrand,
|
||||
padAbsorbency: padAbsorbency ?? this.padAbsorbency,
|
||||
padInventoryCount: padInventoryCount ?? this.padInventoryCount,
|
||||
lowInventoryThreshold: lowInventoryThreshold ?? this.lowInventoryThreshold,
|
||||
isAutoInventoryEnabled: isAutoInventoryEnabled ?? this.isAutoInventoryEnabled,
|
||||
lowInventoryThreshold:
|
||||
lowInventoryThreshold ?? this.lowInventoryThreshold,
|
||||
isAutoInventoryEnabled:
|
||||
isAutoInventoryEnabled ?? this.isAutoInventoryEnabled,
|
||||
lastInventoryUpdate: lastInventoryUpdate ?? this.lastInventoryUpdate,
|
||||
notifyPeriodEstimate: notifyPeriodEstimate ?? this.notifyPeriodEstimate,
|
||||
notifyPeriodStart: notifyPeriodStart ?? this.notifyPeriodStart,
|
||||
@@ -491,6 +510,9 @@ class UserProfile extends HiveObject {
|
||||
isCalendarProtected: isCalendarProtected ?? this.isCalendarProtected,
|
||||
isSuppliesProtected: isSuppliesProtected ?? this.isSuppliesProtected,
|
||||
teachingPlans: teachingPlans ?? this.teachingPlans,
|
||||
husbandThemeMode: husbandThemeMode ?? this.husbandThemeMode,
|
||||
husbandAccentColor: husbandAccentColor ?? this.husbandAccentColor,
|
||||
useExampleData: useExampleData ?? this.useExampleData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,13 +118,18 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
|
||||
isCalendarProtected: fields[51] == null ? false : fields[51] as bool,
|
||||
isSuppliesProtected: fields[52] == null ? false : fields[52] as bool,
|
||||
teachingPlans: (fields[53] as List?)?.cast<TeachingPlan>(),
|
||||
husbandThemeMode:
|
||||
fields[54] == null ? AppThemeMode.system : fields[54] as AppThemeMode,
|
||||
husbandAccentColor:
|
||||
fields[55] == null ? '0xFF1A3A5C' : fields[55] as String,
|
||||
useExampleData: fields[56] == null ? false : fields[56] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, UserProfile obj) {
|
||||
writer
|
||||
..writeByte(53)
|
||||
..writeByte(56)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -230,7 +235,13 @@ class UserProfileAdapter extends TypeAdapter<UserProfile> {
|
||||
..writeByte(52)
|
||||
..write(obj.isSuppliesProtected)
|
||||
..writeByte(53)
|
||||
..write(obj.teachingPlans);
|
||||
..write(obj.teachingPlans)
|
||||
..writeByte(54)
|
||||
..write(obj.husbandThemeMode)
|
||||
..writeByte(55)
|
||||
..write(obj.husbandAccentColor)
|
||||
..writeByte(56)
|
||||
..write(obj.useExampleData);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -37,7 +37,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
final entries = ref.watch(cycleEntriesProvider);
|
||||
final user = ref.watch(userProfileProvider);
|
||||
final isIrregular = user?.isIrregularCycle ?? false;
|
||||
|
||||
|
||||
int cycleLength = user?.averageCycleLength ?? 28;
|
||||
if (isIrregular) {
|
||||
if (_predictionMode == PredictionMode.short) {
|
||||
@@ -46,237 +46,257 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
cycleLength = user?.maxCycleLength ?? 35;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final lastPeriodStart = user?.lastPeriodStartDate;
|
||||
|
||||
return ProtectedContentWrapper(
|
||||
title: 'Calendar',
|
||||
isProtected: user?.isCalendarProtected ?? false,
|
||||
userProfile: user,
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
title: 'Calendar',
|
||||
isProtected: user?.isCalendarProtected ?? false,
|
||||
userProfile: user,
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Calendar',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildLegendButton(),
|
||||
],
|
||||
),
|
||||
if (isIrregular) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildPredictionToggle(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Calendar
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TableCalendar(
|
||||
firstDay: DateTime.now().subtract(const Duration(days: 365)),
|
||||
lastDay: DateTime.now().add(const Duration(days: 365)),
|
||||
focusedDay: _focusedDay,
|
||||
calendarFormat: _calendarFormat,
|
||||
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
setState(() => _calendarFormat = format);
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
_focusedDay = focusedDay;
|
||||
},
|
||||
calendarStyle: CalendarStyle(
|
||||
outsideDaysVisible: false,
|
||||
defaultTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.charcoal,
|
||||
),
|
||||
weekendTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.charcoal,
|
||||
),
|
||||
todayDecoration: BoxDecoration(
|
||||
color: AppColors.sageGreen.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
todayTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.sageGreen,
|
||||
),
|
||||
selectedDecoration: const BoxDecoration(
|
||||
color: AppColors.sageGreen,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
selectedTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
headerStyle: HeaderStyle(
|
||||
formatButtonVisible: false,
|
||||
titleCentered: true,
|
||||
titleTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color ?? AppColors.charcoal,
|
||||
),
|
||||
leftChevronIcon: Icon(
|
||||
Icons.chevron_left,
|
||||
color: Theme.of(context).iconTheme.color ?? AppColors.warmGray,
|
||||
),
|
||||
rightChevronIcon: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).iconTheme.color ?? AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
daysOfWeekStyle: DaysOfWeekStyle(
|
||||
weekdayStyle: GoogleFonts.outfit(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color ?? AppColors.warmGray,
|
||||
),
|
||||
weekendStyle: GoogleFonts.outfit(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color ?? AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
calendarBuilders: CalendarBuilders(
|
||||
defaultBuilder: (context, day, focusedDay) {
|
||||
return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isSelected: false, isToday: false);
|
||||
},
|
||||
todayBuilder: (context, day, focusedDay) {
|
||||
return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isToday: true);
|
||||
},
|
||||
selectedBuilder: (context, day, focusedDay) {
|
||||
return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isSelected: true);
|
||||
},
|
||||
markerBuilder: (context, date, events) {
|
||||
final entry = _getEntryForDate(date, entries);
|
||||
|
||||
if (entry == null) {
|
||||
final phase =
|
||||
_getPhaseForDate(date, lastPeriodStart, cycleLength);
|
||||
if (phase != null) {
|
||||
return Positioned(
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: _getPhaseColor(phase),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we have an entry, show icons/markers
|
||||
return Positioned(
|
||||
bottom: 4,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
Row(
|
||||
children: [
|
||||
if (entry.isPeriodDay)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.menstrualPhase,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
if (entry.mood != null ||
|
||||
entry.energyLevel != 3 ||
|
||||
entry.hasSymptoms)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.softGold,
|
||||
shape: BoxShape.circle,
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Calendar',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineLarge
|
||||
?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildLegendButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
if (isIrregular) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildPredictionToggle(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Divider / Header for Day Info
|
||||
if (_selectedDay != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Daily Log',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
// Calendar
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TableCalendar(
|
||||
firstDay:
|
||||
DateTime.now().subtract(const Duration(days: 365)),
|
||||
lastDay: DateTime.now().add(const Duration(days: 365)),
|
||||
focusedDay: _focusedDay,
|
||||
calendarFormat: _calendarFormat,
|
||||
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
setState(() => _calendarFormat = format);
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
_focusedDay = focusedDay;
|
||||
},
|
||||
calendarStyle: CalendarStyle(
|
||||
outsideDaysVisible: false,
|
||||
defaultTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color ??
|
||||
AppColors.charcoal,
|
||||
),
|
||||
weekendTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color ??
|
||||
AppColors.charcoal,
|
||||
),
|
||||
todayDecoration: BoxDecoration(
|
||||
color: AppColors.sageGreen.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
todayTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.warmGray,
|
||||
letterSpacing: 1,
|
||||
color: AppColors.sageGreen,
|
||||
),
|
||||
selectedDecoration: const BoxDecoration(
|
||||
color: AppColors.sageGreen,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
selectedTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: Divider(color: AppColors.lightGray)),
|
||||
],
|
||||
headerStyle: HeaderStyle(
|
||||
formatButtonVisible: false,
|
||||
titleCentered: true,
|
||||
titleTextStyle: GoogleFonts.outfit(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color ??
|
||||
AppColors.charcoal,
|
||||
),
|
||||
leftChevronIcon: Icon(
|
||||
Icons.chevron_left,
|
||||
color: Theme.of(context).iconTheme.color ??
|
||||
AppColors.warmGray,
|
||||
),
|
||||
rightChevronIcon: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).iconTheme.color ??
|
||||
AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
daysOfWeekStyle: DaysOfWeekStyle(
|
||||
weekdayStyle: GoogleFonts.outfit(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color ??
|
||||
AppColors.warmGray,
|
||||
),
|
||||
weekendStyle: GoogleFonts.outfit(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color ??
|
||||
AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
calendarBuilders: CalendarBuilders(
|
||||
defaultBuilder: (context, day, focusedDay) {
|
||||
return _buildCalendarDay(day, focusedDay, entries,
|
||||
lastPeriodStart, cycleLength,
|
||||
isSelected: false, isToday: false);
|
||||
},
|
||||
todayBuilder: (context, day, focusedDay) {
|
||||
return _buildCalendarDay(day, focusedDay, entries,
|
||||
lastPeriodStart, cycleLength,
|
||||
isToday: true);
|
||||
},
|
||||
selectedBuilder: (context, day, focusedDay) {
|
||||
return _buildCalendarDay(day, focusedDay, entries,
|
||||
lastPeriodStart, cycleLength,
|
||||
isSelected: true);
|
||||
},
|
||||
markerBuilder: (context, date, events) {
|
||||
final entry = _getEntryForDate(date, entries);
|
||||
|
||||
if (entry == null) {
|
||||
final phase = _getPhaseForDate(
|
||||
date, lastPeriodStart, cycleLength);
|
||||
if (phase != null) {
|
||||
return Positioned(
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: _getPhaseColor(phase),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we have an entry, show icons/markers
|
||||
return Positioned(
|
||||
bottom: 4,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (entry.isPeriodDay)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.menstrualPhase,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
if (entry.mood != null ||
|
||||
entry.energyLevel != 3 ||
|
||||
entry.hasSymptoms)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.softGold,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Day Info (No longer Expanded)
|
||||
_buildDayInfo(
|
||||
_selectedDay!, lastPeriodStart, cycleLength, entries, user),
|
||||
|
||||
const SizedBox(height: 40), // Bottom padding
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Divider / Header for Day Info
|
||||
if (_selectedDay != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Daily Log',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.warmGray,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Divider(color: AppColors.lightGray)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Day Info (No longer Expanded)
|
||||
_buildDayInfo(_selectedDay!, lastPeriodStart, cycleLength,
|
||||
entries, user),
|
||||
|
||||
const SizedBox(height: 40), // Bottom padding
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildPredictionToggle() {
|
||||
@@ -288,9 +308,12 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildToggleItem(PredictionMode.short, 'Short (-)', AppColors.menstrualPhase),
|
||||
_buildToggleItem(PredictionMode.regular, 'Regular', AppColors.sageGreen),
|
||||
_buildToggleItem(PredictionMode.long, 'Long (+)', AppColors.lutealPhase),
|
||||
_buildToggleItem(
|
||||
PredictionMode.short, 'Short (-)', AppColors.menstrualPhase),
|
||||
_buildToggleItem(
|
||||
PredictionMode.regular, 'Regular', AppColors.sageGreen),
|
||||
_buildToggleItem(
|
||||
PredictionMode.long, 'Long (+)', AppColors.lutealPhase),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -415,8 +438,8 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, int cycleLength,
|
||||
List<CycleEntry> entries, UserProfile? user) {
|
||||
Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart,
|
||||
int cycleLength, List<CycleEntry> entries, UserProfile? user) {
|
||||
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
|
||||
final entry = _getEntryForDate(date, entries);
|
||||
|
||||
@@ -487,17 +510,18 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
.bodyMedium
|
||||
?.copyWith(color: AppColors.warmGray),
|
||||
),
|
||||
if (user?.isPadTrackingEnabled == true &&
|
||||
phase != CyclePhase.menstrual &&
|
||||
(user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ?? false)) ...[
|
||||
if (user?.isPadTrackingEnabled == true &&
|
||||
phase != CyclePhase.menstrual &&
|
||||
(user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ??
|
||||
false)) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildPantylinerPrompt(date, null),
|
||||
],
|
||||
] else ...[
|
||||
// Period Detail
|
||||
if (entry.isPeriodDay)
|
||||
_buildDetailRow(Icons.water_drop, 'Period Day',
|
||||
AppColors.menstrualPhase,
|
||||
_buildDetailRow(
|
||||
Icons.water_drop, 'Period Day', AppColors.menstrualPhase,
|
||||
value: entry.flowIntensity?.label),
|
||||
|
||||
// Mood Detail
|
||||
@@ -524,11 +548,11 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
if (user?.isPadTrackingEnabled == true) ...[
|
||||
const SizedBox(height: 16),
|
||||
if (entry.usedPantyliner)
|
||||
_buildDetailRow(Icons.layers_outlined, 'Supplies Used', AppColors.menstrualPhase,
|
||||
_buildDetailRow(Icons.layers_outlined, 'Supplies Used',
|
||||
AppColors.menstrualPhase,
|
||||
value: '${entry.pantylinerCount}'),
|
||||
|
||||
if (!entry.usedPantyliner && !entry.isPeriodDay)
|
||||
_buildPantylinerPrompt(date, entry),
|
||||
_buildPantylinerPrompt(date, entry),
|
||||
],
|
||||
|
||||
// Notes
|
||||
@@ -544,16 +568,15 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.warmGray)),
|
||||
const SizedBox(height: 4),
|
||||
Text(entry.notes!,
|
||||
style: GoogleFonts.outfit(fontSize: 14)),
|
||||
Text(entry.notes!, style: GoogleFonts.outfit(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
|
||||
if (user?.isPadTrackingEnabled == true) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildManualSupplyEntryButton(date),
|
||||
const SizedBox(height: 16),
|
||||
_buildManualSupplyEntryButton(date),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
@@ -581,17 +604,17 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
? Icons.edit_note
|
||||
: Icons.add_circle_outline),
|
||||
label: Text(entry != null ? 'Edit Log' : 'Add Log'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.sageGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.sageGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -608,33 +631,38 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.help_outline, color: AppColors.menstrualPhase, size: 20),
|
||||
const Icon(Icons.help_outline,
|
||||
color: AppColors.menstrualPhase, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Did you use pantyliners today?',
|
||||
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal),
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (entry != null) {
|
||||
ref.read(cycleEntriesProvider.notifier).updateEntry(
|
||||
entry.copyWith(usedPantyliner: true, pantylinerCount: 1),
|
||||
);
|
||||
} else {
|
||||
final newEntry = CycleEntry(
|
||||
id: const Uuid().v4(),
|
||||
date: date,
|
||||
usedPantyliner: true,
|
||||
pantylinerCount: 1,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
ref.read(cycleEntriesProvider.notifier).addEntry(newEntry);
|
||||
}
|
||||
if (entry != null) {
|
||||
ref.read(cycleEntriesProvider.notifier).updateEntry(
|
||||
entry.copyWith(usedPantyliner: true, pantylinerCount: 1),
|
||||
);
|
||||
} else {
|
||||
final newEntry = CycleEntry(
|
||||
id: const Uuid().v4(),
|
||||
date: date,
|
||||
usedPantyliner: true,
|
||||
pantylinerCount: 1,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
ref.read(cycleEntriesProvider.notifier).addEntry(newEntry);
|
||||
}
|
||||
},
|
||||
child: Text('Yes', style: GoogleFonts.outfit(color: AppColors.menstrualPhase, fontWeight: FontWeight.bold)),
|
||||
child: Text('Yes',
|
||||
style: GoogleFonts.outfit(
|
||||
color: AppColors.menstrualPhase,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -656,7 +684,8 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.menstrualPhase,
|
||||
side: const BorderSide(color: AppColors.menstrualPhase),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -832,16 +861,12 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
return entry?.isPeriodDay ?? false;
|
||||
}
|
||||
|
||||
Widget _buildCalendarDay(
|
||||
DateTime day,
|
||||
DateTime focusedDay,
|
||||
List<CycleEntry> entries,
|
||||
DateTime? lastPeriodStart,
|
||||
int cycleLength,
|
||||
Widget _buildCalendarDay(DateTime day, DateTime focusedDay,
|
||||
List<CycleEntry> entries, DateTime? lastPeriodStart, int cycleLength,
|
||||
{bool isSelected = false, bool isToday = false, bool isWeekend = false}) {
|
||||
final phase = _getPhaseForDate(day, lastPeriodStart, cycleLength);
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
|
||||
// Determine the Day of Cycle
|
||||
int? doc;
|
||||
if (lastPeriodStart != null) {
|
||||
@@ -876,14 +901,22 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
// Text style
|
||||
TextStyle textStyle = GoogleFonts.outfit(
|
||||
fontSize: (isOvulationDay || isPeriodStart) ? 18 : 14,
|
||||
fontWeight: (isOvulationDay || isPeriodStart) ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? Colors.white : (isToday ? AppColors.sageGreen : (Theme.of(context).textTheme.bodyMedium?.color)),
|
||||
fontWeight: (isOvulationDay || isPeriodStart)
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: (isToday
|
||||
? AppColors.sageGreen
|
||||
: (Theme.of(context).textTheme.bodyMedium?.color)),
|
||||
);
|
||||
|
||||
if (isOvulationDay) {
|
||||
textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.ovulationPhase);
|
||||
textStyle = textStyle.copyWith(
|
||||
color: isSelected ? Colors.white : AppColors.ovulationPhase);
|
||||
} else if (isPeriodStart) {
|
||||
textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.menstrualPhase);
|
||||
textStyle = textStyle.copyWith(
|
||||
color: isSelected ? Colors.white : AppColors.menstrualPhase);
|
||||
}
|
||||
|
||||
return Container(
|
||||
|
||||
@@ -12,14 +12,14 @@ import '../devotional/devotional_screen.dart';
|
||||
import '../settings/appearance_screen.dart';
|
||||
import '../settings/cycle_settings_screen.dart';
|
||||
import '../settings/relationship_settings_screen.dart';
|
||||
import '../settings/goal_settings_screen.dart';
|
||||
import '../settings/goal_settings_screen.dart';
|
||||
import '../settings/cycle_history_screen.dart';
|
||||
import '../settings/sharing_settings_screen.dart';
|
||||
import '../settings/notification_settings_screen.dart';
|
||||
import '../settings/privacy_settings_screen.dart';
|
||||
import '../settings/supplies_settings_screen.dart';
|
||||
import '../settings/export_data_screen.dart';
|
||||
import '../learn/wife_learn_screen.dart';
|
||||
import '../learn/wife_learn_screen.dart';
|
||||
import '../../widgets/tip_card.dart';
|
||||
import '../../widgets/cycle_ring.dart';
|
||||
import '../../widgets/scripture_card.dart';
|
||||
@@ -37,7 +37,8 @@ class HomeScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedIndex = ref.watch(navigationProvider);
|
||||
final isPadTrackingEnabled = ref.watch(userProfileProvider.select((u) => u?.isPadTrackingEnabled ?? false));
|
||||
final isPadTrackingEnabled = ref.watch(
|
||||
userProfileProvider.select((u) => u?.isPadTrackingEnabled ?? false));
|
||||
|
||||
final List<Widget> tabs;
|
||||
final List<BottomNavigationBarItem> navBarItems;
|
||||
@@ -50,16 +51,38 @@ class HomeScreen extends ConsumerWidget {
|
||||
const LogScreen(),
|
||||
const DevotionalScreen(),
|
||||
const WifeLearnScreen(),
|
||||
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
|
||||
_SettingsTab(
|
||||
onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
|
||||
];
|
||||
navBarItems = [
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.inventory_2_outlined), activeIcon: Icon(Icons.inventory_2), label: 'Supplies'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
activeIcon: Icon(Icons.home),
|
||||
label: 'Home'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.calendar_today_outlined),
|
||||
activeIcon: Icon(Icons.calendar_today),
|
||||
label: 'Calendar'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
activeIcon: Icon(Icons.inventory_2),
|
||||
label: 'Supplies'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.add_circle_outline),
|
||||
activeIcon: Icon(Icons.add_circle),
|
||||
label: 'Log'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.menu_book_outlined),
|
||||
activeIcon: Icon(Icons.menu_book),
|
||||
label: 'Devotional'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.school_outlined),
|
||||
activeIcon: Icon(Icons.school),
|
||||
label: 'Learn'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
activeIcon: Icon(Icons.settings),
|
||||
label: 'Settings'),
|
||||
];
|
||||
} else {
|
||||
tabs = [
|
||||
@@ -68,15 +91,34 @@ class HomeScreen extends ConsumerWidget {
|
||||
const DevotionalScreen(),
|
||||
const LogScreen(),
|
||||
const WifeLearnScreen(),
|
||||
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
|
||||
_SettingsTab(
|
||||
onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
|
||||
];
|
||||
navBarItems = [
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
activeIcon: Icon(Icons.home),
|
||||
label: 'Home'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.calendar_today_outlined),
|
||||
activeIcon: Icon(Icons.calendar_today),
|
||||
label: 'Calendar'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.menu_book_outlined),
|
||||
activeIcon: Icon(Icons.menu_book),
|
||||
label: 'Devotional'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.add_circle_outline),
|
||||
activeIcon: Icon(Icons.add_circle),
|
||||
label: 'Log'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.school_outlined),
|
||||
activeIcon: Icon(Icons.school),
|
||||
label: 'Learn'),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
activeIcon: Icon(Icons.settings),
|
||||
label: 'Settings'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -134,7 +176,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Listen for changes in the cycle info to re-initialize scripture if needed
|
||||
ref.listen<CycleInfo>(currentCycleInfoProvider, (previousCycleInfo, newCycleInfo) {
|
||||
ref.listen<CycleInfo>(currentCycleInfoProvider,
|
||||
(previousCycleInfo, newCycleInfo) {
|
||||
if (previousCycleInfo?.phase != newCycleInfo.phase) {
|
||||
_initializeScripture();
|
||||
}
|
||||
@@ -145,8 +188,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
final translation =
|
||||
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation)) ??
|
||||
BibleTranslation.esv;
|
||||
final role = ref.watch(userProfileProvider.select((u) => u?.role)) ??
|
||||
UserRole.wife;
|
||||
final role =
|
||||
ref.watch(userProfileProvider.select((u) => u?.role)) ?? UserRole.wife;
|
||||
final isMarried =
|
||||
ref.watch(userProfileProvider.select((u) => u?.isMarried)) ?? false;
|
||||
final averageCycleLength =
|
||||
@@ -163,7 +206,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
final maxIndex = scriptureState.maxIndex;
|
||||
|
||||
if (scripture == null) {
|
||||
return const Center(child: CircularProgressIndicator()); // Or some error message
|
||||
return const Center(
|
||||
child: CircularProgressIndicator()); // Or some error message
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
@@ -181,10 +225,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
phase: phase,
|
||||
),
|
||||
),
|
||||
if (phase == CyclePhase.menstrual) ...[
|
||||
const SizedBox(height: 24),
|
||||
const PadTrackerCard(),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
const PadTrackerCard(),
|
||||
const SizedBox(height: 32),
|
||||
// Main Scripture Card with Navigation
|
||||
Stack(
|
||||
@@ -203,8 +245,9 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
left: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios),
|
||||
onPressed: () =>
|
||||
ref.read(scriptureProvider.notifier).getPreviousScripture(),
|
||||
onPressed: () => ref
|
||||
.read(scriptureProvider.notifier)
|
||||
.getPreviousScripture(),
|
||||
color: AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
@@ -212,8 +255,9 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
right: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.arrow_forward_ios),
|
||||
onPressed: () =>
|
||||
ref.read(scriptureProvider.notifier).getNextScripture(),
|
||||
onPressed: () => ref
|
||||
.read(scriptureProvider.notifier)
|
||||
.getNextScripture(),
|
||||
color: AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
@@ -222,16 +266,17 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (maxIndex != null && maxIndex > 1)
|
||||
Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: () => ref.read(scriptureProvider.notifier).getRandomScripture(),
|
||||
icon: const Icon(Icons.shuffle),
|
||||
label: const Text('Random Verse'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
ref.read(scriptureProvider.notifier).getRandomScripture(),
|
||||
icon: const Icon(Icons.shuffle),
|
||||
label: const Text('Random Verse'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Quick Log',
|
||||
@@ -243,8 +288,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
const SizedBox(height: 12),
|
||||
const QuickLogButtons(),
|
||||
const SizedBox(height: 24),
|
||||
if (role == UserRole.wife)
|
||||
_buildWifeTipsSection(context),
|
||||
if (role == UserRole.wife) _buildWifeTipsSection(context),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
@@ -364,18 +408,18 @@ class _SettingsTab extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final name =
|
||||
ref.watch(userProfileProvider.select((u) => u?.name)) ?? 'Guest';
|
||||
final roleSymbol =
|
||||
ref.watch(userProfileProvider.select((u) => u?.role)) ==
|
||||
UserRole.husband
|
||||
? 'HUSBAND'
|
||||
: null;
|
||||
final roleSymbol = ref.watch(userProfileProvider.select((u) => u?.role)) ==
|
||||
UserRole.husband
|
||||
? 'HUSBAND'
|
||||
: null;
|
||||
final relationshipStatus = ref.watch(userProfileProvider
|
||||
.select((u) => u?.relationshipStatus.name.toUpperCase())) ??
|
||||
'SINGLE';
|
||||
final translationLabel =
|
||||
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ??
|
||||
'ESV';
|
||||
final isSingle = ref.watch(userProfileProvider.select((u) => u?.relationshipStatus == RelationshipStatus.single));
|
||||
final translationLabel = ref.watch(
|
||||
userProfileProvider.select((u) => u?.bibleTranslation.label)) ??
|
||||
'ESV';
|
||||
final isSingle = ref.watch(userProfileProvider
|
||||
.select((u) => u?.relationshipStatus == RelationshipStatus.single));
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
@@ -398,8 +442,10 @@ class _SettingsTab extends ConsumerWidget {
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color:
|
||||
Theme.of(context).colorScheme.outline.withOpacity(0.05)),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline
|
||||
.withOpacity(0.05)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -409,8 +455,14 @@ class _SettingsTab extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.7),
|
||||
Theme.of(context).colorScheme.secondary.withOpacity(0.7)
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.7),
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(0.7)
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
@@ -459,14 +511,15 @@ class _SettingsTab extends ConsumerWidget {
|
||||
const SizedBox(height: 24),
|
||||
_buildSettingsGroup(context, 'Preferences', [
|
||||
_buildSettingsTile(
|
||||
context,
|
||||
Icons.notifications_outlined,
|
||||
context,
|
||||
Icons.notifications_outlined,
|
||||
'Notifications',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NotificationSettingsScreen()));
|
||||
builder: (context) =>
|
||||
const NotificationSettingsScreen()));
|
||||
},
|
||||
),
|
||||
_buildSettingsTile(
|
||||
@@ -477,7 +530,8 @@ class _SettingsTab extends ConsumerWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SuppliesSettingsScreen()));
|
||||
builder: (context) =>
|
||||
const SuppliesSettingsScreen()));
|
||||
},
|
||||
),
|
||||
_buildSettingsTile(
|
||||
@@ -488,12 +542,13 @@ class _SettingsTab extends ConsumerWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const RelationshipSettingsScreen()));
|
||||
builder: (context) =>
|
||||
const RelationshipSettingsScreen()));
|
||||
},
|
||||
),
|
||||
_buildSettingsTile(
|
||||
context,
|
||||
Icons.flag_outlined,
|
||||
Icons.flag_outlined,
|
||||
'Cycle Goal',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
@@ -521,8 +576,7 @@ class _SettingsTab extends ConsumerWidget {
|
||||
'My Favorites',
|
||||
onTap: () => _showFavoritesDialog(context, ref),
|
||||
),
|
||||
_buildSettingsTile(
|
||||
context, Icons.security, 'Privacy & Security',
|
||||
_buildSettingsTile(context, Icons.security, 'Privacy & Security',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -562,8 +616,7 @@ class _SettingsTab extends ConsumerWidget {
|
||||
builder: (context) => CycleHistoryScreen()));
|
||||
}),
|
||||
_buildSettingsTile(
|
||||
context, Icons.download_outlined, 'Export Data',
|
||||
onTap: () {
|
||||
context, Icons.download_outlined, 'Export Data', onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -629,7 +682,9 @@ class _SettingsTab extends ConsumerWidget {
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('Unlock'),
|
||||
@@ -648,7 +703,8 @@ class _SettingsTab extends ConsumerWidget {
|
||||
final granted = await _authenticate(context, userProfile.privacyPin!);
|
||||
if (!granted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Incorrect PIN')));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('Incorrect PIN')));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -663,14 +719,16 @@ class _SettingsTab extends ConsumerWidget {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('My Favorites', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
|
||||
title: Text('My Favorites',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'List your favorite comfort foods, snacks, or flowers so your husband knows what to get you!',
|
||||
style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray),
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
@@ -696,8 +754,11 @@ class _SettingsTab extends ConsumerWidget {
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
final updatedProfile = userProfile.copyWith(favoriteFoods: favorites);
|
||||
ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
final updatedProfile =
|
||||
userProfile.copyWith(favoriteFoods: favorites);
|
||||
ref
|
||||
.read(userProfileProvider.notifier)
|
||||
.updateProfile(updatedProfile);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
@@ -710,14 +771,16 @@ class _SettingsTab extends ConsumerWidget {
|
||||
void _showShareDialog(BuildContext context, WidgetRef ref) {
|
||||
// Generate a simple pairing code (in a real app, this would be stored/validated)
|
||||
final userProfile = ref.read(userProfileProvider);
|
||||
final pairingCode = userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123';
|
||||
final pairingCode =
|
||||
userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.share_outlined, color: Theme.of(context).colorScheme.primary),
|
||||
Icon(Icons.share_outlined,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Share with Husband'),
|
||||
],
|
||||
@@ -727,7 +790,8 @@ class _SettingsTab extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Share this code with your husband so he can connect to your cycle data:',
|
||||
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
@@ -735,7 +799,9 @@ class _SettingsTab extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)),
|
||||
border: Border.all(
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.3)),
|
||||
),
|
||||
child: SelectableText(
|
||||
pairingCode,
|
||||
@@ -750,7 +816,8 @@ class _SettingsTab extends ConsumerWidget {
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'He can enter this in his app under Settings > Connect with Wife.',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -868,4 +935,4 @@ Widget _buildTipCard(
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
156
lib/screens/husband/husband_appearance_screen.dart
Normal file
156
lib/screens/husband/husband_appearance_screen.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,23 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../models/teaching_plan.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../services/bible_xml_parser.dart';
|
||||
import '../../services/mock_data_service.dart';
|
||||
|
||||
class HusbandDevotionalScreen extends ConsumerStatefulWidget {
|
||||
const HusbandDevotionalScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<HusbandDevotionalScreen> createState() => _HusbandDevotionalScreenState();
|
||||
ConsumerState<HusbandDevotionalScreen> createState() =>
|
||||
_HusbandDevotionalScreenState();
|
||||
}
|
||||
|
||||
class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScreen> {
|
||||
class _HusbandDevotionalScreenState
|
||||
extends ConsumerState<HusbandDevotionalScreen> {
|
||||
final _parser = BibleXmlParser();
|
||||
Map<String, String> _scriptures = {};
|
||||
bool _loading = true;
|
||||
@@ -34,15 +37,16 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
Future<void> _fetchScriptures() async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null) return;
|
||||
|
||||
|
||||
final translation = user.bibleTranslation;
|
||||
if (translation == _currentTranslation && _scriptures.isNotEmpty) return;
|
||||
|
||||
setState(() => _loading = true);
|
||||
|
||||
try {
|
||||
final assetPath = 'assets/bible_xml/${translation.name.toUpperCase()}.xml';
|
||||
|
||||
final assetPath =
|
||||
'assets/bible_xml/${translation.name.toUpperCase()}.xml';
|
||||
|
||||
// Define verses to fetch
|
||||
final versesToFetch = [
|
||||
'1 Corinthians 11:3',
|
||||
@@ -53,7 +57,7 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
];
|
||||
|
||||
final Map<String, String> results = {};
|
||||
|
||||
|
||||
for (final ref in versesToFetch) {
|
||||
final text = await _parser.getVerseFromAsset(assetPath, ref);
|
||||
results[ref] = text ?? 'Verse not found.';
|
||||
@@ -74,7 +78,8 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
|
||||
void _showAddTeachingDialog([TeachingPlan? existingPlan]) {
|
||||
final titleController = TextEditingController(text: existingPlan?.topic);
|
||||
final scriptureController = TextEditingController(text: existingPlan?.scriptureReference);
|
||||
final scriptureController =
|
||||
TextEditingController(text: existingPlan?.scriptureReference);
|
||||
final notesController = TextEditingController(text: existingPlan?.notes);
|
||||
DateTime selectedDate = existingPlan?.date ?? DateTime.now();
|
||||
|
||||
@@ -125,7 +130,8 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
context: context,
|
||||
initialDate: selectedDate,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
lastDate:
|
||||
DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() => selectedDate = picked);
|
||||
@@ -145,41 +151,50 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (titleController.text.isEmpty) return;
|
||||
if (titleController.text.isEmpty) return;
|
||||
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null) return;
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null) return;
|
||||
|
||||
TeachingPlan newPlan;
|
||||
if (existingPlan != null) {
|
||||
newPlan = existingPlan.copyWith(
|
||||
topic: titleController.text,
|
||||
scriptureReference: scriptureController.text,
|
||||
notes: notesController.text,
|
||||
date: selectedDate,
|
||||
);
|
||||
} else {
|
||||
newPlan = TeachingPlan.create(
|
||||
topic: titleController.text,
|
||||
scriptureReference: scriptureController.text,
|
||||
notes: notesController.text,
|
||||
date: selectedDate,
|
||||
);
|
||||
}
|
||||
TeachingPlan newPlan;
|
||||
if (existingPlan != null) {
|
||||
newPlan = existingPlan.copyWith(
|
||||
topic: titleController.text,
|
||||
scriptureReference: scriptureController.text,
|
||||
notes: notesController.text,
|
||||
date: selectedDate,
|
||||
);
|
||||
} else {
|
||||
newPlan = TeachingPlan.create(
|
||||
topic: titleController.text,
|
||||
scriptureReference: scriptureController.text,
|
||||
notes: notesController.text,
|
||||
date: selectedDate,
|
||||
);
|
||||
}
|
||||
|
||||
List<TeachingPlan> updatedList = List.from(user.teachingPlans ?? []);
|
||||
if (existingPlan != null) {
|
||||
final index = updatedList.indexWhere((p) => p.id == existingPlan.id);
|
||||
if (index != -1) updatedList[index] = newPlan;
|
||||
} else {
|
||||
updatedList.add(newPlan);
|
||||
}
|
||||
List<TeachingPlan> updatedList =
|
||||
List.from(user.teachingPlans ?? []);
|
||||
if (existingPlan != null) {
|
||||
final index =
|
||||
updatedList.indexWhere((p) => p.id == existingPlan.id);
|
||||
if (index != -1) updatedList[index] = newPlan;
|
||||
} else {
|
||||
updatedList.add(newPlan);
|
||||
}
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(teachingPlans: updatedList),
|
||||
);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(teachingPlans: updatedList),
|
||||
);
|
||||
|
||||
if (mounted) Navigator.pop(context);
|
||||
// Trigger notification for new teaching plans
|
||||
if (existingPlan == null) {
|
||||
NotificationService().showTeachingPlanNotification(
|
||||
teacherName: user.name ?? 'Husband',
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
@@ -193,10 +208,11 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null || user.teachingPlans == null) return;
|
||||
|
||||
final updatedList = user.teachingPlans!.where((p) => p.id != plan.id).toList();
|
||||
final updatedList =
|
||||
user.teachingPlans!.where((p) => p.id != plan.id).toList();
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(teachingPlans: updatedList),
|
||||
);
|
||||
user.copyWith(teachingPlans: updatedList),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleComplete(TeachingPlan plan) async {
|
||||
@@ -207,17 +223,17 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
if (p.id == plan.id) return p.copyWith(isCompleted: !p.isCompleted);
|
||||
return p;
|
||||
}).toList();
|
||||
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(teachingPlans: updatedList),
|
||||
);
|
||||
user.copyWith(teachingPlans: updatedList),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userProfileProvider);
|
||||
final upcomingPlans = user?.teachingPlans ?? [];
|
||||
upcomingPlans.sort((a,b) => a.date.compareTo(b.date));
|
||||
upcomingPlans.sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
// Listen for translation changes to re-fetch
|
||||
ref.listen(userProfileProvider, (prev, next) {
|
||||
@@ -253,12 +269,13 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _showAddTeachingDialog(),
|
||||
icon: const Icon(Icons.add_circle, color: AppColors.navyBlue, size: 28),
|
||||
icon: const Icon(Icons.add_circle,
|
||||
color: AppColors.navyBlue, size: 28),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
|
||||
if (upcomingPlans.isEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
@@ -303,39 +320,48 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
onDismissed: (_) => _deletePlan(plan),
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: ListTile(
|
||||
onTap: () => _showAddTeachingDialog(plan),
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
plan.isCompleted ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: plan.isCompleted ? Colors.green : Colors.grey
|
||||
),
|
||||
onPressed: () => _toggleComplete(plan),
|
||||
icon: Icon(
|
||||
plan.isCompleted
|
||||
? Icons.check_circle
|
||||
: Icons.circle_outlined,
|
||||
color: plan.isCompleted
|
||||
? Colors.green
|
||||
: Colors.grey),
|
||||
onPressed: () => _toggleComplete(plan),
|
||||
),
|
||||
title: Text(
|
||||
plan.topic,
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: plan.isCompleted ? TextDecoration.lineThrough : null,
|
||||
decoration: plan.isCompleted
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (plan.scriptureReference.isNotEmpty)
|
||||
Text(plan.scriptureReference, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
Text(plan.scriptureReference,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500)),
|
||||
if (plan.notes.isNotEmpty)
|
||||
Text(
|
||||
plan.notes,
|
||||
maxLines: 2,
|
||||
plan.notes,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
DateFormat.yMMMd().format(plan.date),
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
DateFormat.yMMMd().format(plan.date),
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
isThreeLine: true,
|
||||
@@ -344,8 +370,13 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Prayer Request Section
|
||||
_buildPrayerRequestSection(context, ref, user),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -356,11 +387,12 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
// Combine 1 Timothy verses
|
||||
String timothyText = 'Loading...';
|
||||
if (!_loading) {
|
||||
timothyText = '${_scriptures['1 Timothy 3:4'] ?? '...'} ${_scriptures['1 Timothy 3:5'] ?? ''} ... ${_scriptures['1 Timothy 3:12'] ?? ''}';
|
||||
// Cleanup potential double spaces or missing
|
||||
timothyText = timothyText.replaceAll(' ', ' ').trim();
|
||||
timothyText =
|
||||
'${_scriptures['1 Timothy 3:4'] ?? '...'} ${_scriptures['1 Timothy 3:5'] ?? ''} ... ${_scriptures['1 Timothy 3:12'] ?? ''}';
|
||||
// Cleanup potential double spaces or missing
|
||||
timothyText = timothyText.replaceAll(' ', ' ').trim();
|
||||
}
|
||||
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -376,28 +408,31 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
const Icon(Icons.menu_book, color: Color(0xFF8B5E3C)),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Biblical Principles',
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
version,
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: const Color(0xFF8B5E3C)),
|
||||
),
|
||||
],
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Biblical Principles',
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
version,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 12, color: const Color(0xFF8B5E3C)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildVerseText(
|
||||
'1 Corinthians 11:3',
|
||||
_loading ? 'Loading...' : (_scriptures['1 Corinthians 11:3'] ?? 'Verse not found.'),
|
||||
_loading
|
||||
? 'Loading...'
|
||||
: (_scriptures['1 Corinthians 11:3'] ?? 'Verse not found.'),
|
||||
'Supports family structure under Christ’s authority.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -409,9 +444,11 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
'Qualifications for church elders include managing their own households well.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildVerseText(
|
||||
_buildVerseText(
|
||||
'Titus 1:6',
|
||||
_loading ? 'Loading...' : (_scriptures['Titus 1:6'] ?? 'Verse not found.'),
|
||||
_loading
|
||||
? 'Loading...'
|
||||
: (_scriptures['Titus 1:6'] ?? 'Verse not found.'),
|
||||
'Husbands who lead faithfully at home are seen as candidates for formal spiritual leadership.',
|
||||
),
|
||||
],
|
||||
@@ -433,17 +470,17 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
text,
|
||||
key: ValueKey(text), // Animate change
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 15,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1.4,
|
||||
color: const Color(0xFF3E2723),
|
||||
),
|
||||
fontSize: 15,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1.4,
|
||||
color: const Color(0xFF3E2723),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
@@ -456,4 +493,252 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrayerRequestSection(
|
||||
BuildContext context, WidgetRef ref, UserProfile? user) {
|
||||
// Check if connected (partnerName is set)
|
||||
final isConnected =
|
||||
user?.partnerName != null && (user?.partnerName?.isNotEmpty ?? false);
|
||||
|
||||
// Get today's cycle entry to check for prayer requests
|
||||
final entries = ref.watch(cycleEntriesProvider);
|
||||
final todayEntry = entries.isNotEmpty
|
||||
? entries.firstWhere(
|
||||
(e) => DateUtils.isSameDay(e.date, DateTime.now()),
|
||||
orElse: () => entries.first,
|
||||
)
|
||||
: null;
|
||||
|
||||
final prayerRequest = todayEntry?.prayerRequest;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.lavender.withOpacity(0.15),
|
||||
AppColors.blushPink.withOpacity(0.15),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.lavender.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text('🙏', style: TextStyle(fontSize: 20)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Wife\'s Prayer Requests',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (!isConnected) ...[
|
||||
Text(
|
||||
'Connect with your wife to see her prayer requests and pray for her.',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _showConnectDialog(context, ref),
|
||||
icon: const Icon(Icons.link),
|
||||
label: const Text('Connect with Wife'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (prayerRequest != null && prayerRequest.isNotEmpty) ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${user?.partnerName ?? "Wife"} shared:',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
prayerRequest,
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 15,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1.5,
|
||||
color: AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Praying for her! 🙏'),
|
||||
backgroundColor: AppColors.sageGreen,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.favorite, size: 18),
|
||||
label: const Text('I\'m Praying'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.rose,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.favorite_border,
|
||||
color: AppColors.warmGray, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'No prayer requests today',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Check back later or encourage her to share.',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 12,
|
||||
color: AppColors.warmGray.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showConnectDialog(BuildContext context, WidgetRef ref) {
|
||||
final codeController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: const [
|
||||
Icon(Icons.link, color: AppColors.navyBlue),
|
||||
SizedBox(width: 8),
|
||||
Text('Connect with Wife'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Enter the pairing code from your wife\'s app:',
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: codeController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'e.g., ABC123',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Your wife can find this code in her Devotional screen under "Share with Husband".',
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final code = codeController.text.trim();
|
||||
Navigator.pop(context);
|
||||
|
||||
if (code.isNotEmpty) {
|
||||
// Simulate connection with mock data
|
||||
final mockService = MockDataService();
|
||||
final entries = mockService.generateMockCycleEntries();
|
||||
for (var entry in entries) {
|
||||
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
|
||||
}
|
||||
final mockWife = mockService.generateMockWifeProfile();
|
||||
final currentProfile = ref.read(userProfileProvider);
|
||||
if (currentProfile != null) {
|
||||
final updatedProfile = currentProfile.copyWith(
|
||||
partnerName: mockWife.name,
|
||||
averageCycleLength: mockWife.averageCycleLength,
|
||||
averagePeriodLength: mockWife.averagePeriodLength,
|
||||
lastPeriodStartDate: mockWife.lastPeriodStartDate,
|
||||
);
|
||||
await ref
|
||||
.read(userProfileProvider.notifier)
|
||||
.updateProfile(updatedProfile);
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Connected with wife! 💑'),
|
||||
backgroundColor: AppColors.sageGreen,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Connect'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,7 @@ import '../../theme/app_theme.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../services/mock_data_service.dart';
|
||||
import '../../providers/cycle_provider.dart'; // Ensure cycleEntriesProvider is available
|
||||
import '../settings/appearance_screen.dart';
|
||||
import 'husband_appearance_screen.dart';
|
||||
|
||||
class HusbandSettingsScreen extends ConsumerWidget {
|
||||
const HusbandSettingsScreen({super.key});
|
||||
@@ -72,20 +71,22 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
final mockWife = mockService.generateMockWifeProfile();
|
||||
final currentProfile = ref.read(userProfileProvider);
|
||||
if (currentProfile != null) {
|
||||
final updatedProfile = currentProfile.copyWith(
|
||||
partnerName: mockWife.name,
|
||||
averageCycleLength: mockWife.averageCycleLength,
|
||||
averagePeriodLength: mockWife.averagePeriodLength,
|
||||
lastPeriodStartDate: mockWife.lastPeriodStartDate,
|
||||
favoriteFoods: mockWife.favoriteFoods,
|
||||
);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
final updatedProfile = currentProfile.copyWith(
|
||||
partnerName: mockWife.name,
|
||||
averageCycleLength: mockWife.averageCycleLength,
|
||||
averagePeriodLength: mockWife.averagePeriodLength,
|
||||
lastPeriodStartDate: mockWife.lastPeriodStartDate,
|
||||
favoriteFoods: mockWife.favoriteFoods,
|
||||
);
|
||||
await ref
|
||||
.read(userProfileProvider.notifier)
|
||||
.updateProfile(updatedProfile);
|
||||
}
|
||||
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Demo data loaded')),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Demo data loaded')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,23 +113,26 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...BibleTranslation.values.map((translation) => ListTile(
|
||||
title: Text(
|
||||
translation.label,
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
|
||||
),
|
||||
trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation
|
||||
? const Icon(Icons.check, color: AppColors.sageGreen)
|
||||
: null,
|
||||
onTap: () async {
|
||||
final profile = ref.read(userProfileProvider);
|
||||
if (profile != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
profile.copyWith(bibleTranslation: translation),
|
||||
);
|
||||
}
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
title: Text(
|
||||
translation.label,
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
|
||||
),
|
||||
trailing: ref.watch(userProfileProvider)?.bibleTranslation ==
|
||||
translation
|
||||
? const Icon(Icons.check, color: AppColors.sageGreen)
|
||||
: null,
|
||||
onTap: () async {
|
||||
final profile = ref.read(userProfileProvider);
|
||||
if (profile != null) {
|
||||
await ref
|
||||
.read(userProfileProvider.notifier)
|
||||
.updateProfile(
|
||||
profile.copyWith(bibleTranslation: translation),
|
||||
);
|
||||
}
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -138,104 +142,112 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
void _showConnectDialog(BuildContext context, WidgetRef ref) {
|
||||
final codeController = TextEditingController();
|
||||
bool shareDevotional = true;
|
||||
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.link, color: AppColors.navyBlue),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Connect with Wife'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Enter the pairing code from your wife\'s app:',
|
||||
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: codeController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'e.g., ABC123',
|
||||
border: OutlineInputBorder(),
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.link, color: AppColors.navyBlue),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Connect with Wife'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Enter the pairing code from your wife\'s app:',
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
|
||||
),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Your wife can find this code in her Settings under "Share with Husband".',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: codeController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'e.g., ABC123',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Your wife can find this code in her Settings under "Share with Husband".',
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Checkbox(
|
||||
value: shareDevotional,
|
||||
onChanged: (val) => setState(() => shareDevotional = val ?? true),
|
||||
activeColor: AppColors.navyBlue,
|
||||
),
|
||||
SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Checkbox(
|
||||
value: shareDevotional,
|
||||
onChanged: (val) =>
|
||||
setState(() => shareDevotional = val ?? true),
|
||||
activeColor: AppColors.navyBlue,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Share Devotional Plans',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.charcoal),
|
||||
),
|
||||
Text(
|
||||
'Allow her to see the teaching plans you create.',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Share Devotional Plans',
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColors.charcoal),
|
||||
),
|
||||
Text(
|
||||
'Allow her to see the teaching plans you create.',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 12, color: AppColors.warmGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final code = codeController.text.trim();
|
||||
|
||||
Navigator.pop(context);
|
||||
|
||||
// Update preference
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(isDataShared: shareDevotional)
|
||||
);
|
||||
}
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final code = codeController.text.trim();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Settings updated & Connected!'),
|
||||
backgroundColor: AppColors.sageGreen,
|
||||
),
|
||||
);
|
||||
|
||||
if (code.isNotEmpty) {
|
||||
Navigator.pop(context);
|
||||
|
||||
// Update preference
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(isDataShared: shareDevotional));
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Settings updated & Connected!'),
|
||||
backgroundColor: AppColors.sageGreen,
|
||||
),
|
||||
);
|
||||
|
||||
if (code.isNotEmpty) {
|
||||
// Load demo data as simulation
|
||||
final mockService = MockDataService();
|
||||
final entries = mockService.generateMockCycleEntries();
|
||||
for (var entry in entries) {
|
||||
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
|
||||
await ref
|
||||
.read(cycleEntriesProvider.notifier)
|
||||
.addEntry(entry);
|
||||
}
|
||||
final mockWife = mockService.generateMockWifeProfile();
|
||||
final currentProfile = ref.read(userProfileProvider);
|
||||
@@ -248,18 +260,20 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
lastPeriodStartDate: mockWife.lastPeriodStartDate,
|
||||
favoriteFoods: mockWife.favoriteFoods,
|
||||
);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
await ref
|
||||
.read(userProfileProvider.notifier)
|
||||
.updateProfile(updatedProfile);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Connect'),
|
||||
),
|
||||
child: const Text('Connect'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -268,7 +282,8 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Theme aware colors
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final cardColor = Theme.of(context).cardTheme.color; // Using theme card color
|
||||
final cardColor =
|
||||
Theme.of(context).cardTheme.color; // Using theme card color
|
||||
final textColor = Theme.of(context).textTheme.bodyLarge?.color;
|
||||
|
||||
return SafeArea(
|
||||
@@ -282,7 +297,8 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).textTheme.displayMedium?.color ?? AppColors.navyBlue,
|
||||
color: Theme.of(context).textTheme.displayMedium?.color ??
|
||||
AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -297,7 +313,8 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
leading: Icon(Icons.notifications_outlined,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
title: Text('Notifications',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)),
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w500, color: textColor)),
|
||||
trailing: Switch(value: true, onChanged: (val) {}),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
@@ -305,8 +322,10 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
leading: Icon(Icons.link,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
title: Text('Connect with Wife',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)),
|
||||
trailing: Icon(Icons.chevron_right, color: Theme.of(context).disabledColor),
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w500, color: textColor)),
|
||||
trailing: Icon(Icons.chevron_right,
|
||||
color: Theme.of(context).disabledColor),
|
||||
onTap: () => _showConnectDialog(context, ref),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
@@ -314,18 +333,24 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
leading: Icon(Icons.menu_book_outlined,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
title: Text('Bible Translation',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)),
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w500, color: textColor)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? 'ESV',
|
||||
ref.watch(userProfileProvider
|
||||
.select((u) => u?.bibleTranslation.label)) ??
|
||||
'ESV',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.warmGray,
|
||||
color:
|
||||
Theme.of(context).textTheme.bodyMedium?.color ??
|
||||
AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: Theme.of(context).disabledColor),
|
||||
Icon(Icons.chevron_right,
|
||||
color: Theme.of(context).disabledColor),
|
||||
],
|
||||
),
|
||||
onTap: () => _showTranslationPicker(context, ref),
|
||||
@@ -335,13 +360,15 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
leading: Icon(Icons.palette_outlined,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
title: Text('Appearance',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)),
|
||||
trailing: Icon(Icons.chevron_right, color: Theme.of(context).disabledColor),
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w500, color: textColor)),
|
||||
trailing: Icon(Icons.chevron_right,
|
||||
color: Theme.of(context).disabledColor),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AppearanceScreen(),
|
||||
builder: (context) => const HusbandAppearanceScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,13 @@ import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import 'package:christian_period_tracker/models/user_profile.dart';
|
||||
import 'package:christian_period_tracker/models/cycle_entry.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../models/cycle_entry.dart';
|
||||
import '../home/home_screen.dart';
|
||||
import '../husband/husband_home_screen.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../services/notification_service.dart';
|
||||
|
||||
class OnboardingScreen extends ConsumerStatefulWidget {
|
||||
const OnboardingScreen({super.key});
|
||||
@@ -35,6 +36,10 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
int _maxCycleLength = 35;
|
||||
bool _isPadTrackingEnabled = false;
|
||||
|
||||
// Connection options
|
||||
bool _useExampleData = false;
|
||||
bool _skipPartnerConnection = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
@@ -45,14 +50,15 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
if (_isNavigating) return;
|
||||
_isNavigating = true;
|
||||
|
||||
// Husband Flow: Role (0) -> Name (1) -> Finish
|
||||
// Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4)
|
||||
// Husband Flow: Role (0) -> Name (1) -> Connect (2) -> Finish
|
||||
// Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4) -> [Connect (5) if married]
|
||||
|
||||
int nextPage = _currentPage + 1;
|
||||
|
||||
// Logic for skipping pages
|
||||
if (_role == UserRole.husband) {
|
||||
if (_currentPage == 1) {
|
||||
if (_currentPage == 2) {
|
||||
// Finish after connect page
|
||||
await _completeOnboarding();
|
||||
return;
|
||||
}
|
||||
@@ -63,10 +69,21 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
// Skip fertility goal (page 3) if not married
|
||||
nextPage = 4;
|
||||
}
|
||||
if (_currentPage == 4 &&
|
||||
_relationshipStatus != RelationshipStatus.married) {
|
||||
// Skip connect page (page 5) if not married - finish now
|
||||
await _completeOnboarding();
|
||||
return;
|
||||
}
|
||||
if (_currentPage == 5) {
|
||||
// Finish after connect page (married wife)
|
||||
await _completeOnboarding();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextPage <= 4) {
|
||||
// Max pages
|
||||
final maxPages = _role == UserRole.husband ? 2 : 5;
|
||||
if (nextPage <= maxPages) {
|
||||
await _pageController.animateToPage(
|
||||
nextPage,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
@@ -124,18 +141,24 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
? _fertilityGoal
|
||||
: null,
|
||||
averageCycleLength: _averageCycleLength,
|
||||
minCycleLength: _minCycleLength,
|
||||
maxCycleLength: _maxCycleLength,
|
||||
lastPeriodStartDate: _lastPeriodStart,
|
||||
isIrregularCycle: _isIrregularCycle,
|
||||
isPadTrackingEnabled: _isPadTrackingEnabled,
|
||||
hasCompletedOnboarding: true,
|
||||
useExampleData: _useExampleData,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(userProfile);
|
||||
|
||||
// Trigger partner connection notification if applicable
|
||||
if (!_skipPartnerConnection && !_useExampleData) {
|
||||
await NotificationService().showPartnerUpdateNotification(
|
||||
title: 'Connection Successful!',
|
||||
body: 'You are now connected with your partner. Tap to start sharing.',
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// Navigate to appropriate home screen
|
||||
if (_role == UserRole.husband) {
|
||||
@@ -174,7 +197,11 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SmoothPageIndicator(
|
||||
controller: _pageController,
|
||||
count: isHusband ? 2 : 5,
|
||||
count: isHusband
|
||||
? 3
|
||||
: (_relationshipStatus == RelationshipStatus.married
|
||||
? 6
|
||||
: 5),
|
||||
effect: WormEffect(
|
||||
dotHeight: 8,
|
||||
dotWidth: 8,
|
||||
@@ -190,17 +217,22 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
physics:
|
||||
const NeverScrollableScrollPhysics(), // Disable swipe
|
||||
physics: const NeverScrollableScrollPhysics(), // Disable swipe
|
||||
onPageChanged: (index) {
|
||||
setState(() => _currentPage = index);
|
||||
},
|
||||
children: [
|
||||
_buildRolePage(), // Page 0
|
||||
_buildNamePage(), // Page 1
|
||||
_buildRelationshipPage(), // Page 2 (Wife only)
|
||||
_buildFertilityGoalPage(), // Page 3 (Wife married only)
|
||||
_buildCyclePage(), // Page 4 (Wife only)
|
||||
if (_role == UserRole.husband)
|
||||
_buildHusbandConnectPage() // Page 2 (Husband only)
|
||||
else ...[
|
||||
_buildRelationshipPage(), // Page 2 (Wife only)
|
||||
_buildFertilityGoalPage(), // Page 3 (Wife married only)
|
||||
_buildCyclePage(), // Page 4 (Wife only)
|
||||
if (_relationshipStatus == RelationshipStatus.married)
|
||||
_buildWifeConnectPage(), // Page 5 (Wife married only)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -223,10 +255,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.blushPink,
|
||||
AppColors.rose.withOpacity(0.7)
|
||||
],
|
||||
colors: [AppColors.blushPink, AppColors.rose.withOpacity(0.7)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
@@ -304,9 +333,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? activeColor
|
||||
: theme.colorScheme.surfaceVariant,
|
||||
color:
|
||||
isSelected ? activeColor : theme.colorScheme.surfaceVariant,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
@@ -340,8 +368,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Icon(Icons.check_circle, color: activeColor),
|
||||
if (isSelected) Icon(Icons.check_circle, color: activeColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -351,8 +378,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
Widget _buildNamePage() {
|
||||
final theme = Theme.of(context);
|
||||
final isHusband = _role == UserRole.husband;
|
||||
final activeColor =
|
||||
isHusband ? AppColors.navyBlue : AppColors.sageGreen;
|
||||
final activeColor = isHusband ? AppColors.navyBlue : AppColors.sageGreen;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
@@ -413,9 +439,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: ElevatedButton(
|
||||
onPressed: (_name.isNotEmpty && !_isNavigating)
|
||||
? _nextPage
|
||||
: null,
|
||||
onPressed:
|
||||
(_name.isNotEmpty && !_isNavigating) ? _nextPage : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: activeColor,
|
||||
),
|
||||
@@ -557,11 +582,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface)),
|
||||
const SizedBox(height: 32),
|
||||
_buildGoalOption(
|
||||
FertilityGoal.tryingToConceive,
|
||||
'Trying to Conceive',
|
||||
'Track fertile days',
|
||||
Icons.child_care_outlined),
|
||||
_buildGoalOption(FertilityGoal.tryingToConceive, 'Trying to Conceive',
|
||||
'Track fertile days', Icons.child_care_outlined),
|
||||
const SizedBox(height: 12),
|
||||
_buildGoalOption(
|
||||
FertilityGoal.tryingToAvoid,
|
||||
@@ -692,8 +714,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
),
|
||||
Text('$_averageCycleLength days',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.sageGreen)),
|
||||
fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -720,12 +741,14 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: RangeSlider(
|
||||
values: RangeValues(_minCycleLength.toDouble(), _maxCycleLength.toDouble()),
|
||||
values: RangeValues(
|
||||
_minCycleLength.toDouble(), _maxCycleLength.toDouble()),
|
||||
min: 21,
|
||||
max: 45,
|
||||
divisions: 24,
|
||||
activeColor: AppColors.sageGreen,
|
||||
labels: RangeLabels('$_minCycleLength days', '$_maxCycleLength days'),
|
||||
labels: RangeLabels(
|
||||
'$_minCycleLength days', '$_maxCycleLength days'),
|
||||
onChanged: (values) {
|
||||
setState(() {
|
||||
_minCycleLength = values.start.round();
|
||||
@@ -739,8 +762,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
Center(
|
||||
child: Text('$_minCycleLength - $_maxCycleLength days',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.sageGreen)),
|
||||
fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -840,4 +862,267 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Husband Connect Page - Choose to connect with wife or use example data
|
||||
Widget _buildHusbandConnectPage() {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.navyBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(Icons.link, size: 32, color: AppColors.navyBlue),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Connect with your wife',
|
||||
style: theme.textTheme.displaySmall?.copyWith(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'See her cycle info and prayer requests to support her better.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Option 1: Connect with Wife (placeholder for now)
|
||||
_buildConnectOption(
|
||||
icon: Icons.qr_code_scanner,
|
||||
title: 'Connect with Wife',
|
||||
subtitle: 'Enter connection code from her app',
|
||||
isSelected: !_useExampleData,
|
||||
onTap: () => setState(() => _useExampleData = false),
|
||||
color: AppColors.navyBlue,
|
||||
isDark: isDark,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Option 2: Use Example Data
|
||||
_buildConnectOption(
|
||||
icon: Icons.auto_awesome,
|
||||
title: 'Use Example Data',
|
||||
subtitle: 'Explore the app with sample data',
|
||||
isSelected: _useExampleData,
|
||||
onTap: () => setState(() => _useExampleData = true),
|
||||
color: AppColors.navyBlue,
|
||||
isDark: isDark,
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: OutlinedButton(
|
||||
onPressed: _previousPage,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.navyBlue,
|
||||
side: const BorderSide(color: AppColors.navyBlue),
|
||||
),
|
||||
child: const Text('Back'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: ElevatedButton(
|
||||
onPressed: !_isNavigating ? _nextPage : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.navyBlue,
|
||||
),
|
||||
child: const Text('Finish Setup'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Wife Connect Page - Invite husband or skip
|
||||
Widget _buildWifeConnectPage() {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.sageGreen.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(Icons.favorite, size: 32, color: AppColors.sageGreen),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Invite your husband',
|
||||
style: theme.textTheme.displaySmall?.copyWith(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Share your cycle info and prayer requests so he can support you.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Option 1: Invite Husband
|
||||
_buildConnectOption(
|
||||
icon: Icons.share,
|
||||
title: 'Invite Husband',
|
||||
subtitle: 'Generate a connection code to share',
|
||||
isSelected: !_skipPartnerConnection,
|
||||
onTap: () => setState(() => _skipPartnerConnection = false),
|
||||
color: AppColors.sageGreen,
|
||||
isDark: isDark,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Option 2: Skip for Now
|
||||
_buildConnectOption(
|
||||
icon: Icons.schedule,
|
||||
title: 'Skip for Now',
|
||||
subtitle: 'You can invite him later in settings',
|
||||
isSelected: _skipPartnerConnection,
|
||||
onTap: () => setState(() => _skipPartnerConnection = true),
|
||||
color: AppColors.sageGreen,
|
||||
isDark: isDark,
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: OutlinedButton(
|
||||
onPressed: _previousPage,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.sageGreen,
|
||||
side: const BorderSide(color: AppColors.sageGreen),
|
||||
),
|
||||
child: const Text('Back'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: ElevatedButton(
|
||||
onPressed: !_isNavigating ? _nextPage : null,
|
||||
child: const Text('Get Started'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper for connection option cards
|
||||
Widget _buildConnectOption({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
required Color color,
|
||||
required bool isDark,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? color.withOpacity(isDark ? 0.3 : 0.1)
|
||||
: theme.cardTheme.color,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected ? color : theme.colorScheme.outline.withOpacity(0.1),
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color : theme.colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected) Icon(Icons.check_circle, color: color),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,13 @@ class NotificationService {
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
);
|
||||
|
||||
|
||||
// Linux initialization (optional, but good for completeness)
|
||||
final LinuxInitializationSettings initializationSettingsLinux =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
|
||||
final InitializationSettings initializationSettings = InitializationSettings(
|
||||
final InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
iOS: initializationSettingsDarwin,
|
||||
macOS: initializationSettingsDarwin,
|
||||
@@ -63,10 +64,10 @@ class NotificationService {
|
||||
required DateTime scheduledDate,
|
||||
}) async {
|
||||
if (kIsWeb) {
|
||||
// Web platform limitation: Background scheduling is complex.
|
||||
// For this demo/web preview, we'll just log it or rely on the UI confirmation.
|
||||
print('Web Notification Scheduled: $title - $body at $scheduledDate');
|
||||
return;
|
||||
// Web platform limitation: Background scheduling is complex.
|
||||
// For this demo/web preview, we'll just log it or rely on the UI confirmation.
|
||||
print('Web Notification Scheduled: $title - $body at $scheduledDate');
|
||||
return;
|
||||
}
|
||||
|
||||
await flutterLocalNotificationsPlugin.zonedSchedule(
|
||||
@@ -92,30 +93,72 @@ class NotificationService {
|
||||
|
||||
// New method for specific notification types
|
||||
Future<void> showLocalNotification({
|
||||
required int id,
|
||||
required String title,
|
||||
required String body,
|
||||
String? channelId,
|
||||
String? channelName,
|
||||
required int id,
|
||||
required String title,
|
||||
required String body,
|
||||
String? channelId,
|
||||
String? channelName,
|
||||
}) async {
|
||||
if (kIsWeb) {
|
||||
print('Web Local Notification: $title - $body');
|
||||
return;
|
||||
}
|
||||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'tracker_general', 'General Notifications',
|
||||
AndroidNotificationDetails('tracker_general', 'General Notifications',
|
||||
channelDescription: 'General app notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
ticker: 'ticker');
|
||||
|
||||
|
||||
const NotificationDetails notificationDetails =
|
||||
NotificationDetails(android: androidNotificationDetails);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
id, title, body, notificationDetails,
|
||||
payload: 'item x');
|
||||
|
||||
await flutterLocalNotificationsPlugin
|
||||
.show(id, title, body, notificationDetails, payload: 'item x');
|
||||
}
|
||||
|
||||
Future<void> showPrayerRequestNotification(
|
||||
{required String senderName}) async {
|
||||
await showLocalNotification(
|
||||
id: 300,
|
||||
title: 'New Prayer Request',
|
||||
body: '$senderName sent you a prayer request. Tap to pray with them.',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showTeachingPlanNotification(
|
||||
{required String teacherName}) async {
|
||||
await showLocalNotification(
|
||||
id: 302,
|
||||
title: 'New Teaching Plan',
|
||||
body: '$teacherName added a new teaching plan for you.',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showPartnerUpdateNotification(
|
||||
{required String title, required String body}) async {
|
||||
await showLocalNotification(
|
||||
id: 305,
|
||||
title: title,
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showCycleUpdateNotification({required String message}) async {
|
||||
await showLocalNotification(
|
||||
id: 310,
|
||||
title: 'Cycle Update',
|
||||
body: message,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showSymptomNotification(
|
||||
{required String senderName, required String symptom}) async {
|
||||
await showLocalNotification(
|
||||
id: 315,
|
||||
title: 'Partner Care Reminder',
|
||||
body: '$senderName logged $symptom. A little extra care might be nice!',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cancelNotification(int id) async {
|
||||
|
||||
@@ -17,6 +17,9 @@ class PadTrackerCard extends ConsumerStatefulWidget {
|
||||
class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
|
||||
Timer? _timer;
|
||||
String _timeDisplay = '';
|
||||
double _progress = 0.0;
|
||||
Color _statusColor = AppColors.menstrualPhase;
|
||||
bool _isCountDown = true; // Toggle state
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -40,35 +43,92 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
|
||||
void _updateTime() {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user?.lastPadChangeTime == null) {
|
||||
if (mounted) setState(() => _timeDisplay = 'Tap to start');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_timeDisplay = 'Tap to start';
|
||||
_progress = 0;
|
||||
_statusColor = AppColors.menstrualPhase;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(user!.lastPadChangeTime!);
|
||||
|
||||
// We want to show time SINCE change (duration worn)
|
||||
final hours = difference.inHours;
|
||||
final minutes = difference.inMinutes.remainder(60);
|
||||
final seconds = difference.inSeconds.remainder(60);
|
||||
|
||||
// Estimate max duration based on flow
|
||||
// None/Precautionary: 8h, Spotting: 8h, Light: 6h, Medium: 4h, Heavy: 3h
|
||||
final flowIntensity = user.typicalFlowIntensity ?? 2; // Default to light
|
||||
Duration maxDuration;
|
||||
switch (flowIntensity) {
|
||||
case 0: // No Flow / Precautionary (health guideline: 8h max)
|
||||
maxDuration = const Duration(hours: 8);
|
||||
break;
|
||||
case 1: // Spotting
|
||||
maxDuration = const Duration(hours: 8);
|
||||
break;
|
||||
case 2: // Light
|
||||
maxDuration = const Duration(hours: 6);
|
||||
break;
|
||||
case 3: // Medium
|
||||
maxDuration = const Duration(hours: 4);
|
||||
break;
|
||||
case 4: // Heavy
|
||||
maxDuration = const Duration(hours: 3);
|
||||
break;
|
||||
case 5: // Very Heavy
|
||||
maxDuration = const Duration(hours: 2);
|
||||
break;
|
||||
default:
|
||||
maxDuration = const Duration(hours: 4);
|
||||
}
|
||||
|
||||
final totalSeconds = maxDuration.inSeconds;
|
||||
final elapsedSeconds = difference.inSeconds;
|
||||
double progress = elapsedSeconds / totalSeconds;
|
||||
progress = progress.clamp(0.0, 1.0);
|
||||
|
||||
// Determine Status Color
|
||||
Color newColor = AppColors.menstrualPhase;
|
||||
if (progress > 0.9) {
|
||||
newColor = Colors.red;
|
||||
} else if (progress > 0.75) {
|
||||
newColor = Colors.orange;
|
||||
} else {
|
||||
newColor = AppColors.menstrualPhase; // Greenish/Theme color
|
||||
}
|
||||
// Override if we want to visually show "fresh" vs "old"
|
||||
|
||||
String text = '';
|
||||
|
||||
if (user.showPadTimerMinutes) {
|
||||
if (hours > 0) text += '$hours hr ';
|
||||
text += '$minutes min';
|
||||
}
|
||||
|
||||
if (user.showPadTimerSeconds) {
|
||||
if (text.isNotEmpty) text += ' ';
|
||||
text += '$seconds sec';
|
||||
}
|
||||
|
||||
if (text.isEmpty) text = 'Active'; // Fallback
|
||||
if (_isCountDown) {
|
||||
final remaining = maxDuration - difference;
|
||||
final isOverdue = remaining.isNegative;
|
||||
final absRemaining = remaining.abs();
|
||||
final hours = absRemaining.inHours;
|
||||
final mins = absRemaining.inMinutes % 60;
|
||||
|
||||
if (isOverdue) {
|
||||
text = 'Overdue by ${hours}h ${mins}m';
|
||||
newColor = Colors.red; // Force red if overdue
|
||||
} else {
|
||||
text = '${hours}h ${mins}m left';
|
||||
}
|
||||
_progress =
|
||||
isOverdue ? 1.0 : (1.0 - progress); // Depleting if not overdue
|
||||
} else {
|
||||
// Count Up
|
||||
final hours = difference.inHours;
|
||||
final minutes = difference.inMinutes % 60;
|
||||
text = '${hours}h ${minutes}m worn';
|
||||
_progress = progress; // Filling
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_timeDisplay = text;
|
||||
_progress = _isCountDown && text.contains('Overdue') ? 1.0 : _progress;
|
||||
_statusColor = newColor;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -76,10 +136,8 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userProfileProvider);
|
||||
if (user == null || !user.isPadTrackingEnabled) return const SizedBox.shrink();
|
||||
|
||||
// Re-check time on rebuilds in case settings changed
|
||||
// _updateTime(); // Actually let the timer handle it, or use a key to rebuild on setting changes
|
||||
if (user == null || !user.isPadTrackingEnabled)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@@ -91,53 +149,86 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.menstrualPhase.withOpacity(0.1),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)),
|
||||
border: Border.all(color: _statusColor.withOpacity(0.3)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.menstrualPhase.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
BoxShadow(
|
||||
color: _statusColor.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.menstrualPhase.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.timer_outlined, color: AppColors.menstrualPhase, size: 24),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: _statusColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child:
|
||||
Icon(Icons.timer_outlined, color: _statusColor, size: 24),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Pad Tracker',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isCountDown = !_isCountDown;
|
||||
_updateTime();
|
||||
});
|
||||
},
|
||||
child: Icon(
|
||||
_isCountDown
|
||||
? Icons.arrow_downward
|
||||
: Icons.arrow_upward,
|
||||
size: 16,
|
||||
color: AppColors.warmGray),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_timeDisplay.isNotEmpty ? _timeDisplay : 'Tap to track',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _statusColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: AppColors.lightGray),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Pad Tracker',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_timeDisplay.isNotEmpty ? _timeDisplay : 'Tap to track',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.menstrualPhase
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: _progress,
|
||||
backgroundColor: _statusColor.withOpacity(0.1),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(_statusColor),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: AppColors.menstrualPhase),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import '../providers/user_provider.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../providers/navigation_provider.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import 'quick_log_dialog.dart';
|
||||
|
||||
class QuickLogButtons extends ConsumerWidget {
|
||||
@@ -63,6 +64,13 @@ class QuickLogButtons extends ConsumerWidget {
|
||||
color: AppColors.lutealPhase,
|
||||
onTap: () => _showQuickLogDialog(context, 'pads'),
|
||||
),
|
||||
_buildQuickButton(
|
||||
context,
|
||||
icon: Icons.church_outlined,
|
||||
label: 'Prayer',
|
||||
color: AppColors.softGold, // Or a suitable color
|
||||
onTap: () => _showQuickLogDialog(context, 'prayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -96,8 +104,7 @@ class QuickLogButtons extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(isDark ? 0.2 : 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border:
|
||||
isDark ? Border.all(color: color.withOpacity(0.3)) : null,
|
||||
border: isDark ? Border.all(color: color.withOpacity(0.3)) : null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -23,7 +23,7 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
FlowIntensity? _flowIntensity;
|
||||
MoodLevel? _mood;
|
||||
int? _energyLevel;
|
||||
|
||||
|
||||
// Symptoms & Cravings
|
||||
final Map<String, bool> _symptoms = {
|
||||
'Headache': false,
|
||||
@@ -37,7 +37,7 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
'Insomnia': false,
|
||||
'Cramps': false,
|
||||
};
|
||||
|
||||
|
||||
final TextEditingController _cravingController = TextEditingController();
|
||||
List<String> _cravings = [];
|
||||
List<String> _recentCravings = [];
|
||||
@@ -102,6 +102,8 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
return _buildSymptomsLog();
|
||||
case 'cravings':
|
||||
return _buildCravingsLog();
|
||||
case 'prayer':
|
||||
return _buildPrayerLog();
|
||||
default:
|
||||
return const Text('Invalid log type.');
|
||||
}
|
||||
@@ -137,7 +139,7 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
}
|
||||
|
||||
Widget _buildCravingsLog() {
|
||||
return Container(
|
||||
return Container(
|
||||
width: double.maxFinite,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -152,37 +154,44 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
setState(() {
|
||||
_cravings.add(value.trim());
|
||||
_cravingController.clear();
|
||||
});
|
||||
setState(() {
|
||||
_cravings.add(value.trim());
|
||||
_cravingController.clear();
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: _cravings.map((c) => Chip(
|
||||
label: Text(c),
|
||||
onDeleted: () {
|
||||
setState(() => _cravings.remove(c));
|
||||
},
|
||||
)).toList(),
|
||||
children: _cravings
|
||||
.map((c) => Chip(
|
||||
label: Text(c),
|
||||
onDeleted: () {
|
||||
setState(() => _cravings.remove(c));
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_recentCravings.isNotEmpty) ...[
|
||||
Text('Recent Cravings:', style: GoogleFonts.outfit(fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
Text('Recent Cravings:',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: _recentCravings.take(5).map((c) => ActionChip(
|
||||
label: Text(c),
|
||||
onPressed: () {
|
||||
if (!_cravings.contains(c)) {
|
||||
setState(() => _cravings.add(c));
|
||||
}
|
||||
},
|
||||
)).toList(),
|
||||
children: _recentCravings
|
||||
.take(5)
|
||||
.map((c) => ActionChip(
|
||||
label: Text(c),
|
||||
onPressed: () {
|
||||
if (!_cravings.contains(c)) {
|
||||
setState(() => _cravings.add(c));
|
||||
}
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
]
|
||||
],
|
||||
@@ -277,10 +286,30 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrayerLog() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Enter your prayer request or gratitude:'),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller:
|
||||
_cravingController, // Reusing controller for simplicity, or create _prayerController
|
||||
maxLines: 4,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'I am thankful for...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveLog() async {
|
||||
// Handle text input for cravings if user didn't hit enter
|
||||
if (widget.logType == 'cravings' && _cravingController.text.isNotEmpty) {
|
||||
_cravings.add(_cravingController.text.trim());
|
||||
_cravings.add(_cravingController.text.trim());
|
||||
}
|
||||
|
||||
final cycleNotifier = ref.read(cycleEntriesProvider.notifier);
|
||||
@@ -288,7 +317,11 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
final entries = ref.read(cycleEntriesProvider);
|
||||
final entry = entries.firstWhere(
|
||||
(e) => DateUtils.isSameDay(e.date, today),
|
||||
orElse: () => CycleEntry(id: const Uuid().v4(), date: today, createdAt: today, updatedAt: today),
|
||||
orElse: () => CycleEntry(
|
||||
id: const Uuid().v4(),
|
||||
date: today,
|
||||
createdAt: today,
|
||||
updatedAt: today),
|
||||
);
|
||||
|
||||
CycleEntry updatedEntry = entry;
|
||||
@@ -308,28 +341,61 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
break;
|
||||
case 'symptoms':
|
||||
updatedEntry = entry.copyWith(
|
||||
hasHeadache: _symptoms['Headache'],
|
||||
hasBloating: _symptoms['Bloating'],
|
||||
hasBreastTenderness: _symptoms['Breast Tenderness'],
|
||||
hasFatigue: _symptoms['Fatigue'],
|
||||
hasAcne: _symptoms['Acne'],
|
||||
hasLowerBackPain: _symptoms['Back Pain'],
|
||||
hasConstipation: _symptoms['Constipation'],
|
||||
hasDiarrhea: _symptoms['Diarrhea'],
|
||||
hasInsomnia: _symptoms['Insomnia'],
|
||||
crampIntensity: _symptoms['Cramps'] == true ? 2 : 0, // Default to mild cramps if just toggled
|
||||
hasHeadache: _symptoms['Headache'],
|
||||
hasBloating: _symptoms['Bloating'],
|
||||
hasBreastTenderness: _symptoms['Breast Tenderness'],
|
||||
hasFatigue: _symptoms['Fatigue'],
|
||||
hasAcne: _symptoms['Acne'],
|
||||
hasLowerBackPain: _symptoms['Back Pain'],
|
||||
hasConstipation: _symptoms['Constipation'],
|
||||
hasDiarrhea: _symptoms['Diarrhea'],
|
||||
hasInsomnia: _symptoms['Insomnia'],
|
||||
crampIntensity: _symptoms['Cramps'] == true
|
||||
? 2
|
||||
: 0, // Default to mild cramps if just toggled
|
||||
);
|
||||
// Trigger notification if any symptom is selected
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (_symptoms.values.any((selected) => selected == true)) {
|
||||
final selectedSymptom = _symptoms.entries
|
||||
.firstWhere((element) => element.value == true)
|
||||
.key;
|
||||
NotificationService().showSymptomNotification(
|
||||
senderName: user?.name ?? 'Wife',
|
||||
symptom: selectedSymptom,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'cravings':
|
||||
final currentCravings = entry.cravings ?? [];
|
||||
final newCravings = {...currentCravings, ..._cravings}.toList();
|
||||
updatedEntry = entry.copyWith(cravings: newCravings);
|
||||
|
||||
|
||||
// Update History
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final history = prefs.getStringList('recent_cravings') ?? [];
|
||||
final updatedHistory = {..._cravings, ...history}.take(20).toList();
|
||||
await prefs.setStringList('recent_cravings', updatedHistory);
|
||||
await prefs.setStringList('recent_cravings', updatedHistory);
|
||||
break;
|
||||
case 'prayer':
|
||||
final currentPrayer = entry.prayerRequest ?? '';
|
||||
final newPrayer =
|
||||
_cravingController.text.trim(); // Using reused controller
|
||||
if (newPrayer.isNotEmpty) {
|
||||
updatedEntry = entry.copyWith(
|
||||
prayerRequest: currentPrayer.isEmpty
|
||||
? newPrayer
|
||||
: '$currentPrayer\n$newPrayer');
|
||||
|
||||
// Trigger notification
|
||||
final user = ref.read(userProfileProvider);
|
||||
NotificationService().showPrayerRequestNotification(
|
||||
senderName: user?.name ?? 'Wife',
|
||||
);
|
||||
} else {
|
||||
return; // Don't save empty prayer
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// pads handled separately
|
||||
|
||||
Reference in New Issue
Block a user