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