diff --git a/android/app/build.gradle b/android/app/build.gradle index efdf820..60af6fa 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,10 +7,11 @@ plugins { android { namespace = "com.faithapps.christian_period_tracker" - compileSdk = flutter.compileSdkVersion + compileSdk = 36 ndkVersion = flutter.ndkVersion compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } @@ -24,7 +25,7 @@ android { applicationId = "com.faithapps.christian_period_tracker" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + minSdk = 26 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName @@ -39,6 +40,17 @@ android { } } +configurations.all { + resolutionStrategy { + force 'androidx.core:core:1.13.1' + force 'androidx.core:core-ktx:1.13.1' + } +} + +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' +} + flutter { source = "../.." } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index d2146d2..19cfad9 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,14 +1,5 @@ -<<<<<<< HEAD -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip -======= -#Fri Dec 19 21:26:00 CST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ->>>>>>> 6742220 (Your commit message here) diff --git a/android/settings.gradle b/android/settings.gradle index b9e43bd..10fd726 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false + id "com.android.application" version "8.6.0" apply false id "org.jetbrains.kotlin.android" version "1.8.22" apply false } diff --git a/lib/models/cycle_entry.dart b/lib/models/cycle_entry.dart index efdcbb5..c3870e6 100644 --- a/lib/models/cycle_entry.dart +++ b/lib/models/cycle_entry.dart @@ -27,6 +27,9 @@ enum MoodLevel { /// Flow intensity for period days @HiveType(typeId: 4) enum FlowIntensity { + @HiveField(4) + none, // No flow / Precautionary + @HiveField(0) spotting, @@ -166,7 +169,8 @@ class CycleEntry extends HiveObject { String? husbandNotes; // Separate notes for husband @HiveField(29) - bool? intimacyProtected; // null = no selection, true = protected, false = unprotected + bool? + intimacyProtected; // null = no selection, true = protected, false = unprotected @HiveField(30, defaultValue: false) bool usedPantyliner; @@ -174,6 +178,9 @@ class CycleEntry extends HiveObject { @HiveField(31, defaultValue: 0) int pantylinerCount; + @HiveField(32) + String? prayerRequest; + CycleEntry({ required this.id, required this.date, @@ -207,6 +214,7 @@ class CycleEntry extends HiveObject { this.husbandNotes, this.usedPantyliner = false, this.pantylinerCount = 0, + this.prayerRequest, }); List get _symptomsList => [ @@ -271,6 +279,7 @@ class CycleEntry extends HiveObject { String? husbandNotes, bool? usedPantyliner, int? pantylinerCount, + String? prayerRequest, }) { return CycleEntry( id: id ?? this.id, @@ -292,7 +301,8 @@ class CycleEntry extends HiveObject { hasInsomnia: hasInsomnia ?? this.hasInsomnia, basalBodyTemperature: basalBodyTemperature ?? this.basalBodyTemperature, cervicalMucus: cervicalMucus ?? this.cervicalMucus, - ovulationTestPositive: ovulationTestPositive ?? this.ovulationTestPositive, + ovulationTestPositive: + ovulationTestPositive ?? this.ovulationTestPositive, notes: notes ?? this.notes, cravings: cravings ?? this.cravings, sleepHours: sleepHours ?? this.sleepHours, @@ -305,6 +315,7 @@ class CycleEntry extends HiveObject { husbandNotes: husbandNotes ?? this.husbandNotes, usedPantyliner: usedPantyliner ?? this.usedPantyliner, pantylinerCount: pantylinerCount ?? this.pantylinerCount, + prayerRequest: prayerRequest ?? this.prayerRequest, ); } } @@ -345,6 +356,8 @@ extension MoodLevelExtension on MoodLevel { extension FlowIntensityExtension on FlowIntensity { String get label { switch (this) { + case FlowIntensity.none: + return 'No Flow'; case FlowIntensity.spotting: return 'Spotting'; case FlowIntensity.light: @@ -410,11 +423,7 @@ extension CyclePhaseExtension on CyclePhase { case CyclePhase.ovulation: return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose]; case CyclePhase.luteal: - return [ - AppColors.lutealPhase, - AppColors.lavender, - AppColors.blushPink - ]; + return [AppColors.lutealPhase, AppColors.lavender, AppColors.blushPink]; } } diff --git a/lib/models/cycle_entry.g.dart b/lib/models/cycle_entry.g.dart index f88acca..2b5f379 100644 --- a/lib/models/cycle_entry.g.dart +++ b/lib/models/cycle_entry.g.dart @@ -49,13 +49,14 @@ class CycleEntryAdapter extends TypeAdapter { husbandNotes: fields[28] as String?, usedPantyliner: fields[30] == null ? false : fields[30] as bool, pantylinerCount: fields[31] == null ? 0 : fields[31] as int, + prayerRequest: fields[32] as String?, ); } @override void write(BinaryWriter writer, CycleEntry obj) { writer - ..writeByte(32) + ..writeByte(33) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -119,7 +120,9 @@ class CycleEntryAdapter extends TypeAdapter { ..writeByte(30) ..write(obj.usedPantyliner) ..writeByte(31) - ..write(obj.pantylinerCount); + ..write(obj.pantylinerCount) + ..writeByte(32) + ..write(obj.prayerRequest); } @override @@ -194,6 +197,8 @@ class FlowIntensityAdapter extends TypeAdapter { @override FlowIntensity read(BinaryReader reader) { switch (reader.readByte()) { + case 4: + return FlowIntensity.none; case 0: return FlowIntensity.spotting; case 1: @@ -203,13 +208,16 @@ class FlowIntensityAdapter extends TypeAdapter { case 3: return FlowIntensity.heavy; default: - return FlowIntensity.spotting; + return FlowIntensity.none; } } @override void write(BinaryWriter writer, FlowIntensity obj) { switch (obj) { + case FlowIntensity.none: + writer.writeByte(4); + break; case FlowIntensity.spotting: writer.writeByte(0); break; diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart index 60ee161..2e734a1 100644 --- a/lib/models/user_profile.dart +++ b/lib/models/user_profile.dart @@ -302,6 +302,17 @@ class UserProfile extends HiveObject { @HiveField(53) List? teachingPlans; + // Husband-specific theme settings + @HiveField(54, defaultValue: AppThemeMode.system) + AppThemeMode husbandThemeMode; + + @HiveField(55, defaultValue: '0xFF1A3A5C') + String husbandAccentColor; + + // Whether to use example/demo data (for husband not connected to wife) + @HiveField(56, defaultValue: false) + bool useExampleData; + UserProfile({ required this.id, required this.name, @@ -356,6 +367,9 @@ class UserProfile extends HiveObject { this.isCalendarProtected = false, this.isSuppliesProtected = false, this.teachingPlans, + this.husbandThemeMode = AppThemeMode.system, + this.husbandAccentColor = '0xFF1A3A5C', + this.useExampleData = false, }); /// Check if user is married @@ -435,6 +449,9 @@ class UserProfile extends HiveObject { bool? isCalendarProtected, bool? isSuppliesProtected, List? teachingPlans, + AppThemeMode? husbandThemeMode, + String? husbandAccentColor, + bool? useExampleData, }) { return UserProfile( id: id ?? this.id, @@ -470,8 +487,10 @@ class UserProfile extends HiveObject { padBrand: padBrand ?? this.padBrand, padAbsorbency: padAbsorbency ?? this.padAbsorbency, padInventoryCount: padInventoryCount ?? this.padInventoryCount, - lowInventoryThreshold: lowInventoryThreshold ?? this.lowInventoryThreshold, - isAutoInventoryEnabled: isAutoInventoryEnabled ?? this.isAutoInventoryEnabled, + lowInventoryThreshold: + lowInventoryThreshold ?? this.lowInventoryThreshold, + isAutoInventoryEnabled: + isAutoInventoryEnabled ?? this.isAutoInventoryEnabled, lastInventoryUpdate: lastInventoryUpdate ?? this.lastInventoryUpdate, notifyPeriodEstimate: notifyPeriodEstimate ?? this.notifyPeriodEstimate, notifyPeriodStart: notifyPeriodStart ?? this.notifyPeriodStart, @@ -491,6 +510,9 @@ class UserProfile extends HiveObject { isCalendarProtected: isCalendarProtected ?? this.isCalendarProtected, isSuppliesProtected: isSuppliesProtected ?? this.isSuppliesProtected, teachingPlans: teachingPlans ?? this.teachingPlans, + husbandThemeMode: husbandThemeMode ?? this.husbandThemeMode, + husbandAccentColor: husbandAccentColor ?? this.husbandAccentColor, + useExampleData: useExampleData ?? this.useExampleData, ); } } diff --git a/lib/models/user_profile.g.dart b/lib/models/user_profile.g.dart index fe38eaa..3d09bc3 100644 --- a/lib/models/user_profile.g.dart +++ b/lib/models/user_profile.g.dart @@ -118,13 +118,18 @@ class UserProfileAdapter extends TypeAdapter { isCalendarProtected: fields[51] == null ? false : fields[51] as bool, isSuppliesProtected: fields[52] == null ? false : fields[52] as bool, teachingPlans: (fields[53] as List?)?.cast(), + husbandThemeMode: + fields[54] == null ? AppThemeMode.system : fields[54] as AppThemeMode, + husbandAccentColor: + fields[55] == null ? '0xFF1A3A5C' : fields[55] as String, + useExampleData: fields[56] == null ? false : fields[56] as bool, ); } @override void write(BinaryWriter writer, UserProfile obj) { writer - ..writeByte(53) + ..writeByte(56) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -230,7 +235,13 @@ class UserProfileAdapter extends TypeAdapter { ..writeByte(52) ..write(obj.isSuppliesProtected) ..writeByte(53) - ..write(obj.teachingPlans); + ..write(obj.teachingPlans) + ..writeByte(54) + ..write(obj.husbandThemeMode) + ..writeByte(55) + ..write(obj.husbandAccentColor) + ..writeByte(56) + ..write(obj.useExampleData); } @override diff --git a/lib/screens/calendar/calendar_screen.dart b/lib/screens/calendar/calendar_screen.dart index 57457b5..89fd0ad 100644 --- a/lib/screens/calendar/calendar_screen.dart +++ b/lib/screens/calendar/calendar_screen.dart @@ -37,7 +37,7 @@ class _CalendarScreenState extends ConsumerState { final entries = ref.watch(cycleEntriesProvider); final user = ref.watch(userProfileProvider); final isIrregular = user?.isIrregularCycle ?? false; - + int cycleLength = user?.averageCycleLength ?? 28; if (isIrregular) { if (_predictionMode == PredictionMode.short) { @@ -46,237 +46,257 @@ class _CalendarScreenState extends ConsumerState { cycleLength = user?.maxCycleLength ?? 35; } } - + final lastPeriodStart = user?.lastPeriodStartDate; return ProtectedContentWrapper( - title: 'Calendar', - isProtected: user?.isCalendarProtected ?? false, - userProfile: user, - child: SafeArea( - child: SingleChildScrollView( - child: Column( - children: [ - // Header - Padding( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - Row( + title: 'Calendar', + isProtected: user?.isCalendarProtected ?? false, + userProfile: user, + child: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + // Header + Padding( + padding: const EdgeInsets.all(20), + child: Column( children: [ - Expanded( - child: Text( - 'Calendar', - style: GoogleFonts.outfit( - fontSize: 28, - fontWeight: FontWeight.w600, - color: AppColors.charcoal, - ), - ), - ), - _buildLegendButton(), - ], - ), - if (isIrregular) ...[ - const SizedBox(height: 16), - _buildPredictionToggle(), - ], - ], - ), - ), - - // Calendar - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: TableCalendar( - firstDay: DateTime.now().subtract(const Duration(days: 365)), - lastDay: DateTime.now().add(const Duration(days: 365)), - focusedDay: _focusedDay, - calendarFormat: _calendarFormat, - selectedDayPredicate: (day) => isSameDay(_selectedDay, day), - onDaySelected: (selectedDay, focusedDay) { - setState(() { - _selectedDay = selectedDay; - _focusedDay = focusedDay; - }); - }, - onFormatChanged: (format) { - setState(() => _calendarFormat = format); - }, - onPageChanged: (focusedDay) { - _focusedDay = focusedDay; - }, - calendarStyle: CalendarStyle( - outsideDaysVisible: false, - defaultTextStyle: GoogleFonts.outfit( - fontSize: 14, - color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.charcoal, - ), - weekendTextStyle: GoogleFonts.outfit( - fontSize: 14, - color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.charcoal, - ), - todayDecoration: BoxDecoration( - color: AppColors.sageGreen.withOpacity(0.3), - shape: BoxShape.circle, - ), - todayTextStyle: GoogleFonts.outfit( - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppColors.sageGreen, - ), - selectedDecoration: const BoxDecoration( - color: AppColors.sageGreen, - shape: BoxShape.circle, - ), - selectedTextStyle: GoogleFonts.outfit( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - headerStyle: HeaderStyle( - formatButtonVisible: false, - titleCentered: true, - titleTextStyle: GoogleFonts.outfit( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.titleLarge?.color ?? AppColors.charcoal, - ), - leftChevronIcon: Icon( - Icons.chevron_left, - color: Theme.of(context).iconTheme.color ?? AppColors.warmGray, - ), - rightChevronIcon: Icon( - Icons.chevron_right, - color: Theme.of(context).iconTheme.color ?? AppColors.warmGray, - ), - ), - daysOfWeekStyle: DaysOfWeekStyle( - weekdayStyle: GoogleFonts.outfit( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodySmall?.color ?? AppColors.warmGray, - ), - weekendStyle: GoogleFonts.outfit( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodySmall?.color ?? AppColors.warmGray, - ), - ), - calendarBuilders: CalendarBuilders( - defaultBuilder: (context, day, focusedDay) { - return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isSelected: false, isToday: false); - }, - todayBuilder: (context, day, focusedDay) { - return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isToday: true); - }, - selectedBuilder: (context, day, focusedDay) { - return _buildCalendarDay(day, focusedDay, entries, lastPeriodStart, cycleLength, isSelected: true); - }, - markerBuilder: (context, date, events) { - final entry = _getEntryForDate(date, entries); - - if (entry == null) { - final phase = - _getPhaseForDate(date, lastPeriodStart, cycleLength); - if (phase != null) { - return Positioned( - bottom: 4, - child: Container( - width: 5, - height: 5, - decoration: BoxDecoration( - color: _getPhaseColor(phase), - shape: BoxShape.circle, - ), - ), - ); - } - return null; - } - - // If we have an entry, show icons/markers - return Positioned( - bottom: 4, - child: Row( - mainAxisSize: MainAxisSize.min, + Row( children: [ - if (entry.isPeriodDay) - Container( - width: 6, - height: 6, - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: const BoxDecoration( - color: AppColors.menstrualPhase, - shape: BoxShape.circle, - ), - ), - if (entry.mood != null || - entry.energyLevel != 3 || - entry.hasSymptoms) - Container( - width: 6, - height: 6, - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: const BoxDecoration( - color: AppColors.softGold, - shape: BoxShape.circle, + Expanded( + child: Text( + 'Calendar', + style: GoogleFonts.outfit( + fontSize: 28, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .textTheme + .headlineLarge + ?.color, ), ), + ), + _buildLegendButton(), ], ), - ); - }, + if (isIrregular) ...[ + const SizedBox(height: 16), + _buildPredictionToggle(), + ], + ], + ), ), - ), - ), - const SizedBox(height: 24), - - // Divider / Header for Day Info - if (_selectedDay != null) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - Text( - 'Daily Log', - style: GoogleFonts.outfit( - fontSize: 16, + // Calendar + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: TableCalendar( + firstDay: + DateTime.now().subtract(const Duration(days: 365)), + lastDay: DateTime.now().add(const Duration(days: 365)), + focusedDay: _focusedDay, + calendarFormat: _calendarFormat, + selectedDayPredicate: (day) => isSameDay(_selectedDay, day), + onDaySelected: (selectedDay, focusedDay) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + }); + }, + onFormatChanged: (format) { + setState(() => _calendarFormat = format); + }, + onPageChanged: (focusedDay) { + _focusedDay = focusedDay; + }, + calendarStyle: CalendarStyle( + outsideDaysVisible: false, + defaultTextStyle: GoogleFonts.outfit( + fontSize: 14, + color: Theme.of(context).textTheme.bodyMedium?.color ?? + AppColors.charcoal, + ), + weekendTextStyle: GoogleFonts.outfit( + fontSize: 14, + color: Theme.of(context).textTheme.bodyMedium?.color ?? + AppColors.charcoal, + ), + todayDecoration: BoxDecoration( + color: AppColors.sageGreen.withOpacity(0.3), + shape: BoxShape.circle, + ), + todayTextStyle: GoogleFonts.outfit( + fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.warmGray, - letterSpacing: 1, + color: AppColors.sageGreen, + ), + selectedDecoration: const BoxDecoration( + color: AppColors.sageGreen, + shape: BoxShape.circle, + ), + selectedTextStyle: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, ), ), - const SizedBox(width: 12), - const Expanded(child: Divider(color: AppColors.lightGray)), - ], + headerStyle: HeaderStyle( + formatButtonVisible: false, + titleCentered: true, + titleTextStyle: GoogleFonts.outfit( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context).textTheme.titleLarge?.color ?? + AppColors.charcoal, + ), + leftChevronIcon: Icon( + Icons.chevron_left, + color: Theme.of(context).iconTheme.color ?? + AppColors.warmGray, + ), + rightChevronIcon: Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color ?? + AppColors.warmGray, + ), + ), + daysOfWeekStyle: DaysOfWeekStyle( + weekdayStyle: GoogleFonts.outfit( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.bodySmall?.color ?? + AppColors.warmGray, + ), + weekendStyle: GoogleFonts.outfit( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.bodySmall?.color ?? + AppColors.warmGray, + ), + ), + calendarBuilders: CalendarBuilders( + defaultBuilder: (context, day, focusedDay) { + return _buildCalendarDay(day, focusedDay, entries, + lastPeriodStart, cycleLength, + isSelected: false, isToday: false); + }, + todayBuilder: (context, day, focusedDay) { + return _buildCalendarDay(day, focusedDay, entries, + lastPeriodStart, cycleLength, + isToday: true); + }, + selectedBuilder: (context, day, focusedDay) { + return _buildCalendarDay(day, focusedDay, entries, + lastPeriodStart, cycleLength, + isSelected: true); + }, + markerBuilder: (context, date, events) { + final entry = _getEntryForDate(date, entries); + + if (entry == null) { + final phase = _getPhaseForDate( + date, lastPeriodStart, cycleLength); + if (phase != null) { + return Positioned( + bottom: 4, + child: Container( + width: 5, + height: 5, + decoration: BoxDecoration( + color: _getPhaseColor(phase), + shape: BoxShape.circle, + ), + ), + ); + } + return null; + } + + // If we have an entry, show icons/markers + return Positioned( + bottom: 4, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (entry.isPeriodDay) + Container( + width: 6, + height: 6, + margin: + const EdgeInsets.symmetric(horizontal: 1), + decoration: const BoxDecoration( + color: AppColors.menstrualPhase, + shape: BoxShape.circle, + ), + ), + if (entry.mood != null || + entry.energyLevel != 3 || + entry.hasSymptoms) + Container( + width: 6, + height: 6, + margin: + const EdgeInsets.symmetric(horizontal: 1), + decoration: const BoxDecoration( + color: AppColors.softGold, + shape: BoxShape.circle, + ), + ), + ], + ), + ); + }, + ), + ), ), - ), - const SizedBox(height: 12), - - // Day Info (No longer Expanded) - _buildDayInfo( - _selectedDay!, lastPeriodStart, cycleLength, entries, user), - - const SizedBox(height: 40), // Bottom padding - ], - ], - ), - ), - )); + + const SizedBox(height: 24), + + // Divider / Header for Day Info + if (_selectedDay != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Text( + 'Daily Log', + style: GoogleFonts.outfit( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.warmGray, + letterSpacing: 1, + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Divider(color: AppColors.lightGray)), + ], + ), + ), + const SizedBox(height: 12), + + // Day Info (No longer Expanded) + _buildDayInfo(_selectedDay!, lastPeriodStart, cycleLength, + entries, user), + + const SizedBox(height: 40), // Bottom padding + ], + ], + ), + ), + )); } Widget _buildPredictionToggle() { @@ -288,9 +308,12 @@ class _CalendarScreenState extends ConsumerState { ), child: Row( children: [ - _buildToggleItem(PredictionMode.short, 'Short (-)', AppColors.menstrualPhase), - _buildToggleItem(PredictionMode.regular, 'Regular', AppColors.sageGreen), - _buildToggleItem(PredictionMode.long, 'Long (+)', AppColors.lutealPhase), + _buildToggleItem( + PredictionMode.short, 'Short (-)', AppColors.menstrualPhase), + _buildToggleItem( + PredictionMode.regular, 'Regular', AppColors.sageGreen), + _buildToggleItem( + PredictionMode.long, 'Long (+)', AppColors.lutealPhase), ], ), ); @@ -415,8 +438,8 @@ class _CalendarScreenState extends ConsumerState { ); } - Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, int cycleLength, - List entries, UserProfile? user) { + Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, + int cycleLength, List entries, UserProfile? user) { final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength); final entry = _getEntryForDate(date, entries); @@ -487,17 +510,18 @@ class _CalendarScreenState extends ConsumerState { .bodyMedium ?.copyWith(color: AppColors.warmGray), ), - if (user?.isPadTrackingEnabled == true && - phase != CyclePhase.menstrual && - (user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ?? false)) ...[ + if (user?.isPadTrackingEnabled == true && + phase != CyclePhase.menstrual && + (user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ?? + false)) ...[ const SizedBox(height: 16), _buildPantylinerPrompt(date, null), ], ] else ...[ // Period Detail if (entry.isPeriodDay) - _buildDetailRow(Icons.water_drop, 'Period Day', - AppColors.menstrualPhase, + _buildDetailRow( + Icons.water_drop, 'Period Day', AppColors.menstrualPhase, value: entry.flowIntensity?.label), // Mood Detail @@ -524,11 +548,11 @@ class _CalendarScreenState extends ConsumerState { if (user?.isPadTrackingEnabled == true) ...[ const SizedBox(height: 16), if (entry.usedPantyliner) - _buildDetailRow(Icons.layers_outlined, 'Supplies Used', AppColors.menstrualPhase, + _buildDetailRow(Icons.layers_outlined, 'Supplies Used', + AppColors.menstrualPhase, value: '${entry.pantylinerCount}'), - if (!entry.usedPantyliner && !entry.isPeriodDay) - _buildPantylinerPrompt(date, entry), + _buildPantylinerPrompt(date, entry), ], // Notes @@ -544,16 +568,15 @@ class _CalendarScreenState extends ConsumerState { fontWeight: FontWeight.w600, color: AppColors.warmGray)), const SizedBox(height: 4), - Text(entry.notes!, - style: GoogleFonts.outfit(fontSize: 14)), + Text(entry.notes!, style: GoogleFonts.outfit(fontSize: 14)), ], ), ), ], - + if (user?.isPadTrackingEnabled == true) ...[ - const SizedBox(height: 16), - _buildManualSupplyEntryButton(date), + const SizedBox(height: 16), + _buildManualSupplyEntryButton(date), ], const SizedBox(height: 24), @@ -581,17 +604,17 @@ class _CalendarScreenState extends ConsumerState { ? Icons.edit_note : Icons.add_circle_outline), label: Text(entry != null ? 'Edit Log' : 'Add Log'), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.sageGreen, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12)), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.sageGreen, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), ), ), - ), - ], - ), + ], + ), ], ), ); @@ -608,33 +631,38 @@ class _CalendarScreenState extends ConsumerState { ), child: Row( children: [ - const Icon(Icons.help_outline, color: AppColors.menstrualPhase, size: 20), + const Icon(Icons.help_outline, + color: AppColors.menstrualPhase, size: 20), const SizedBox(width: 12), Expanded( child: Text( 'Did you use pantyliners today?', - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal), + style: + GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal), ), ), TextButton( onPressed: () { - if (entry != null) { - ref.read(cycleEntriesProvider.notifier).updateEntry( - entry.copyWith(usedPantyliner: true, pantylinerCount: 1), - ); - } else { - final newEntry = CycleEntry( - id: const Uuid().v4(), - date: date, - usedPantyliner: true, - pantylinerCount: 1, - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - ref.read(cycleEntriesProvider.notifier).addEntry(newEntry); - } + if (entry != null) { + ref.read(cycleEntriesProvider.notifier).updateEntry( + entry.copyWith(usedPantyliner: true, pantylinerCount: 1), + ); + } else { + final newEntry = CycleEntry( + id: const Uuid().v4(), + date: date, + usedPantyliner: true, + pantylinerCount: 1, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + ref.read(cycleEntriesProvider.notifier).addEntry(newEntry); + } }, - child: Text('Yes', style: GoogleFonts.outfit(color: AppColors.menstrualPhase, fontWeight: FontWeight.bold)), + child: Text('Yes', + style: GoogleFonts.outfit( + color: AppColors.menstrualPhase, + fontWeight: FontWeight.bold)), ), ], ), @@ -656,7 +684,8 @@ class _CalendarScreenState extends ConsumerState { style: OutlinedButton.styleFrom( foregroundColor: AppColors.menstrualPhase, side: const BorderSide(color: AppColors.menstrualPhase), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ); @@ -832,16 +861,12 @@ class _CalendarScreenState extends ConsumerState { return entry?.isPeriodDay ?? false; } - Widget _buildCalendarDay( - DateTime day, - DateTime focusedDay, - List entries, - DateTime? lastPeriodStart, - int cycleLength, + Widget _buildCalendarDay(DateTime day, DateTime focusedDay, + List entries, DateTime? lastPeriodStart, int cycleLength, {bool isSelected = false, bool isToday = false, bool isWeekend = false}) { final phase = _getPhaseForDate(day, lastPeriodStart, cycleLength); final isDark = Theme.of(context).brightness == Brightness.dark; - + // Determine the Day of Cycle int? doc; if (lastPeriodStart != null) { @@ -876,14 +901,22 @@ class _CalendarScreenState extends ConsumerState { // Text style TextStyle textStyle = GoogleFonts.outfit( fontSize: (isOvulationDay || isPeriodStart) ? 18 : 14, - fontWeight: (isOvulationDay || isPeriodStart) ? FontWeight.bold : FontWeight.normal, - color: isSelected ? Colors.white : (isToday ? AppColors.sageGreen : (Theme.of(context).textTheme.bodyMedium?.color)), + fontWeight: (isOvulationDay || isPeriodStart) + ? FontWeight.bold + : FontWeight.normal, + color: isSelected + ? Colors.white + : (isToday + ? AppColors.sageGreen + : (Theme.of(context).textTheme.bodyMedium?.color)), ); if (isOvulationDay) { - textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.ovulationPhase); + textStyle = textStyle.copyWith( + color: isSelected ? Colors.white : AppColors.ovulationPhase); } else if (isPeriodStart) { - textStyle = textStyle.copyWith(color: isSelected ? Colors.white : AppColors.menstrualPhase); + textStyle = textStyle.copyWith( + color: isSelected ? Colors.white : AppColors.menstrualPhase); } return Container( diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 413f138..23e6788 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -12,14 +12,14 @@ import '../devotional/devotional_screen.dart'; import '../settings/appearance_screen.dart'; import '../settings/cycle_settings_screen.dart'; import '../settings/relationship_settings_screen.dart'; -import '../settings/goal_settings_screen.dart'; +import '../settings/goal_settings_screen.dart'; import '../settings/cycle_history_screen.dart'; import '../settings/sharing_settings_screen.dart'; import '../settings/notification_settings_screen.dart'; import '../settings/privacy_settings_screen.dart'; import '../settings/supplies_settings_screen.dart'; import '../settings/export_data_screen.dart'; -import '../learn/wife_learn_screen.dart'; +import '../learn/wife_learn_screen.dart'; import '../../widgets/tip_card.dart'; import '../../widgets/cycle_ring.dart'; import '../../widgets/scripture_card.dart'; @@ -37,7 +37,8 @@ class HomeScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedIndex = ref.watch(navigationProvider); - final isPadTrackingEnabled = ref.watch(userProfileProvider.select((u) => u?.isPadTrackingEnabled ?? false)); + final isPadTrackingEnabled = ref.watch( + userProfileProvider.select((u) => u?.isPadTrackingEnabled ?? false)); final List tabs; final List navBarItems; @@ -50,16 +51,38 @@ class HomeScreen extends ConsumerWidget { const LogScreen(), const DevotionalScreen(), const WifeLearnScreen(), - _SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)), + _SettingsTab( + onReset: () => ref.read(navigationProvider.notifier).setIndex(0)), ]; navBarItems = [ - const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'), - const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'), - const BottomNavigationBarItem(icon: Icon(Icons.inventory_2_outlined), activeIcon: Icon(Icons.inventory_2), label: 'Supplies'), - const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'), - const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'), - const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'), - const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'), + const BottomNavigationBarItem( + icon: Icon(Icons.home_outlined), + activeIcon: Icon(Icons.home), + label: 'Home'), + const BottomNavigationBarItem( + icon: Icon(Icons.calendar_today_outlined), + activeIcon: Icon(Icons.calendar_today), + label: 'Calendar'), + const BottomNavigationBarItem( + icon: Icon(Icons.inventory_2_outlined), + activeIcon: Icon(Icons.inventory_2), + label: 'Supplies'), + const BottomNavigationBarItem( + icon: Icon(Icons.add_circle_outline), + activeIcon: Icon(Icons.add_circle), + label: 'Log'), + const BottomNavigationBarItem( + icon: Icon(Icons.menu_book_outlined), + activeIcon: Icon(Icons.menu_book), + label: 'Devotional'), + const BottomNavigationBarItem( + icon: Icon(Icons.school_outlined), + activeIcon: Icon(Icons.school), + label: 'Learn'), + const BottomNavigationBarItem( + icon: Icon(Icons.settings_outlined), + activeIcon: Icon(Icons.settings), + label: 'Settings'), ]; } else { tabs = [ @@ -68,15 +91,34 @@ class HomeScreen extends ConsumerWidget { const DevotionalScreen(), const LogScreen(), const WifeLearnScreen(), - _SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)), + _SettingsTab( + onReset: () => ref.read(navigationProvider.notifier).setIndex(0)), ]; navBarItems = [ - const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'), - const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'), - const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'), - const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'), - const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'), - const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'), + const BottomNavigationBarItem( + icon: Icon(Icons.home_outlined), + activeIcon: Icon(Icons.home), + label: 'Home'), + const BottomNavigationBarItem( + icon: Icon(Icons.calendar_today_outlined), + activeIcon: Icon(Icons.calendar_today), + label: 'Calendar'), + const BottomNavigationBarItem( + icon: Icon(Icons.menu_book_outlined), + activeIcon: Icon(Icons.menu_book), + label: 'Devotional'), + const BottomNavigationBarItem( + icon: Icon(Icons.add_circle_outline), + activeIcon: Icon(Icons.add_circle), + label: 'Log'), + const BottomNavigationBarItem( + icon: Icon(Icons.school_outlined), + activeIcon: Icon(Icons.school), + label: 'Learn'), + const BottomNavigationBarItem( + icon: Icon(Icons.settings_outlined), + activeIcon: Icon(Icons.settings), + label: 'Settings'), ]; } @@ -134,7 +176,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { @override Widget build(BuildContext context) { // Listen for changes in the cycle info to re-initialize scripture if needed - ref.listen(currentCycleInfoProvider, (previousCycleInfo, newCycleInfo) { + ref.listen(currentCycleInfoProvider, + (previousCycleInfo, newCycleInfo) { if (previousCycleInfo?.phase != newCycleInfo.phase) { _initializeScripture(); } @@ -145,8 +188,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { final translation = ref.watch(userProfileProvider.select((u) => u?.bibleTranslation)) ?? BibleTranslation.esv; - final role = ref.watch(userProfileProvider.select((u) => u?.role)) ?? - UserRole.wife; + final role = + ref.watch(userProfileProvider.select((u) => u?.role)) ?? UserRole.wife; final isMarried = ref.watch(userProfileProvider.select((u) => u?.isMarried)) ?? false; final averageCycleLength = @@ -163,7 +206,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { final maxIndex = scriptureState.maxIndex; if (scripture == null) { - return const Center(child: CircularProgressIndicator()); // Or some error message + return const Center( + child: CircularProgressIndicator()); // Or some error message } return SafeArea( @@ -181,10 +225,8 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { phase: phase, ), ), - if (phase == CyclePhase.menstrual) ...[ - const SizedBox(height: 24), - const PadTrackerCard(), - ], + const SizedBox(height: 24), + const PadTrackerCard(), const SizedBox(height: 32), // Main Scripture Card with Navigation Stack( @@ -203,8 +245,9 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { left: 0, child: IconButton( icon: Icon(Icons.arrow_back_ios), - onPressed: () => - ref.read(scriptureProvider.notifier).getPreviousScripture(), + onPressed: () => ref + .read(scriptureProvider.notifier) + .getPreviousScripture(), color: AppColors.charcoal, ), ), @@ -212,8 +255,9 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { right: 0, child: IconButton( icon: Icon(Icons.arrow_forward_ios), - onPressed: () => - ref.read(scriptureProvider.notifier).getNextScripture(), + onPressed: () => ref + .read(scriptureProvider.notifier) + .getNextScripture(), color: AppColors.charcoal, ), ), @@ -222,16 +266,17 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { ), const SizedBox(height: 16), if (maxIndex != null && maxIndex > 1) - Center( - child: TextButton.icon( - onPressed: () => ref.read(scriptureProvider.notifier).getRandomScripture(), - icon: const Icon(Icons.shuffle), - label: const Text('Random Verse'), - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.primary, + Center( + child: TextButton.icon( + onPressed: () => + ref.read(scriptureProvider.notifier).getRandomScripture(), + icon: const Icon(Icons.shuffle), + label: const Text('Random Verse'), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.primary, + ), ), ), - ), const SizedBox(height: 24), Text( 'Quick Log', @@ -243,8 +288,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { const SizedBox(height: 12), const QuickLogButtons(), const SizedBox(height: 24), - if (role == UserRole.wife) - _buildWifeTipsSection(context), + if (role == UserRole.wife) _buildWifeTipsSection(context), const SizedBox(height: 20), ], ), @@ -364,18 +408,18 @@ class _SettingsTab extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final name = ref.watch(userProfileProvider.select((u) => u?.name)) ?? 'Guest'; - final roleSymbol = - ref.watch(userProfileProvider.select((u) => u?.role)) == - UserRole.husband - ? 'HUSBAND' - : null; + final roleSymbol = ref.watch(userProfileProvider.select((u) => u?.role)) == + UserRole.husband + ? 'HUSBAND' + : null; final relationshipStatus = ref.watch(userProfileProvider .select((u) => u?.relationshipStatus.name.toUpperCase())) ?? 'SINGLE'; - final translationLabel = - ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? - 'ESV'; - final isSingle = ref.watch(userProfileProvider.select((u) => u?.relationshipStatus == RelationshipStatus.single)); + final translationLabel = ref.watch( + userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? + 'ESV'; + final isSingle = ref.watch(userProfileProvider + .select((u) => u?.relationshipStatus == RelationshipStatus.single)); return SafeArea( child: SingleChildScrollView( @@ -398,8 +442,10 @@ class _SettingsTab extends ConsumerWidget { color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(16), border: Border.all( - color: - Theme.of(context).colorScheme.outline.withOpacity(0.05)), + color: Theme.of(context) + .colorScheme + .outline + .withOpacity(0.05)), ), child: Row( children: [ @@ -409,8 +455,14 @@ class _SettingsTab extends ConsumerWidget { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Theme.of(context).colorScheme.primary.withOpacity(0.7), - Theme.of(context).colorScheme.secondary.withOpacity(0.7) + Theme.of(context) + .colorScheme + .primary + .withOpacity(0.7), + Theme.of(context) + .colorScheme + .secondary + .withOpacity(0.7) ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -459,14 +511,15 @@ class _SettingsTab extends ConsumerWidget { const SizedBox(height: 24), _buildSettingsGroup(context, 'Preferences', [ _buildSettingsTile( - context, - Icons.notifications_outlined, + context, + Icons.notifications_outlined, 'Notifications', onTap: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => const NotificationSettingsScreen())); + builder: (context) => + const NotificationSettingsScreen())); }, ), _buildSettingsTile( @@ -477,7 +530,8 @@ class _SettingsTab extends ConsumerWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => const SuppliesSettingsScreen())); + builder: (context) => + const SuppliesSettingsScreen())); }, ), _buildSettingsTile( @@ -488,12 +542,13 @@ class _SettingsTab extends ConsumerWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => const RelationshipSettingsScreen())); + builder: (context) => + const RelationshipSettingsScreen())); }, ), _buildSettingsTile( context, - Icons.flag_outlined, + Icons.flag_outlined, 'Cycle Goal', onTap: () { Navigator.push( @@ -521,8 +576,7 @@ class _SettingsTab extends ConsumerWidget { 'My Favorites', onTap: () => _showFavoritesDialog(context, ref), ), - _buildSettingsTile( - context, Icons.security, 'Privacy & Security', + _buildSettingsTile(context, Icons.security, 'Privacy & Security', onTap: () { Navigator.push( context, @@ -562,8 +616,7 @@ class _SettingsTab extends ConsumerWidget { builder: (context) => CycleHistoryScreen())); }), _buildSettingsTile( - context, Icons.download_outlined, 'Export Data', - onTap: () { + context, Icons.download_outlined, 'Export Data', onTap: () { Navigator.push( context, MaterialPageRoute( @@ -629,7 +682,9 @@ class _SettingsTab extends ConsumerWidget { autofocus: true, ), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel')), ElevatedButton( onPressed: () => Navigator.pop(context, controller.text), child: const Text('Unlock'), @@ -648,7 +703,8 @@ class _SettingsTab extends ConsumerWidget { final granted = await _authenticate(context, userProfile.privacyPin!); if (!granted) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Incorrect PIN'))); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Incorrect PIN'))); } return; } @@ -663,14 +719,16 @@ class _SettingsTab extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: Text('My Favorites', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)), + title: Text('My Favorites', + style: GoogleFonts.outfit(fontWeight: FontWeight.bold)), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'List your favorite comfort foods, snacks, or flowers so your husband knows what to get you!', - style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray), + style: + GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray), ), const SizedBox(height: 16), TextField( @@ -696,8 +754,11 @@ class _SettingsTab extends ConsumerWidget { .where((e) => e.isNotEmpty) .toList(); - final updatedProfile = userProfile.copyWith(favoriteFoods: favorites); - ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); + final updatedProfile = + userProfile.copyWith(favoriteFoods: favorites); + ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); Navigator.pop(context); }, child: const Text('Save'), @@ -710,14 +771,16 @@ class _SettingsTab extends ConsumerWidget { void _showShareDialog(BuildContext context, WidgetRef ref) { // Generate a simple pairing code (in a real app, this would be stored/validated) final userProfile = ref.read(userProfileProvider); - final pairingCode = userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123'; + final pairingCode = + userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123'; showDialog( context: context, builder: (context) => AlertDialog( title: Row( children: [ - Icon(Icons.share_outlined, color: Theme.of(context).colorScheme.primary), + Icon(Icons.share_outlined, + color: Theme.of(context).colorScheme.primary), const SizedBox(width: 8), const Text('Share with Husband'), ], @@ -727,7 +790,8 @@ class _SettingsTab extends ConsumerWidget { children: [ Text( 'Share this code with your husband so he can connect to your cycle data:', - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), + style: + GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), ), const SizedBox(height: 24), Container( @@ -735,7 +799,9 @@ class _SettingsTab extends ConsumerWidget { decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)), + border: Border.all( + color: + Theme.of(context).colorScheme.primary.withOpacity(0.3)), ), child: SelectableText( pairingCode, @@ -750,7 +816,8 @@ class _SettingsTab extends ConsumerWidget { const SizedBox(height: 16), Text( 'He can enter this in his app under Settings > Connect with Wife.', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), + style: + GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), textAlign: TextAlign.center, ), ], @@ -868,4 +935,4 @@ Widget _buildTipCard( ), ], ); -} \ No newline at end of file +} diff --git a/lib/screens/husband/husband_appearance_screen.dart b/lib/screens/husband/husband_appearance_screen.dart new file mode 100644 index 0000000..106fe01 --- /dev/null +++ b/lib/screens/husband/husband_appearance_screen.dart @@ -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( + 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 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(), + ), + ], + ); + } +} diff --git a/lib/screens/husband/husband_devotional_screen.dart b/lib/screens/husband/husband_devotional_screen.dart index ba68926..c42754a 100644 --- a/lib/screens/husband/husband_devotional_screen.dart +++ b/lib/screens/husband/husband_devotional_screen.dart @@ -2,20 +2,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; -import '../../models/user_profile.dart'; +import '../../models/user_profile.dart'; import '../../models/teaching_plan.dart'; import '../../providers/user_provider.dart'; import '../../theme/app_theme.dart'; import '../../services/bible_xml_parser.dart'; +import '../../services/mock_data_service.dart'; class HusbandDevotionalScreen extends ConsumerStatefulWidget { const HusbandDevotionalScreen({super.key}); @override - ConsumerState createState() => _HusbandDevotionalScreenState(); + ConsumerState createState() => + _HusbandDevotionalScreenState(); } -class _HusbandDevotionalScreenState extends ConsumerState { +class _HusbandDevotionalScreenState + extends ConsumerState { final _parser = BibleXmlParser(); Map _scriptures = {}; bool _loading = true; @@ -34,15 +37,16 @@ class _HusbandDevotionalScreenState extends ConsumerState _fetchScriptures() async { final user = ref.read(userProfileProvider); if (user == null) return; - + final translation = user.bibleTranslation; if (translation == _currentTranslation && _scriptures.isNotEmpty) return; setState(() => _loading = true); try { - final assetPath = 'assets/bible_xml/${translation.name.toUpperCase()}.xml'; - + final assetPath = + 'assets/bible_xml/${translation.name.toUpperCase()}.xml'; + // Define verses to fetch final versesToFetch = [ '1 Corinthians 11:3', @@ -53,7 +57,7 @@ class _HusbandDevotionalScreenState extends ConsumerState results = {}; - + for (final ref in versesToFetch) { final text = await _parser.getVerseFromAsset(assetPath, ref); results[ref] = text ?? 'Verse not found.'; @@ -74,7 +78,8 @@ class _HusbandDevotionalScreenState extends ConsumerState selectedDate = picked); @@ -145,41 +151,50 @@ class _HusbandDevotionalScreenState extends ConsumerState updatedList = List.from(user.teachingPlans ?? []); - if (existingPlan != null) { - final index = updatedList.indexWhere((p) => p.id == existingPlan.id); - if (index != -1) updatedList[index] = newPlan; - } else { - updatedList.add(newPlan); - } + List updatedList = + List.from(user.teachingPlans ?? []); + if (existingPlan != null) { + final index = + updatedList.indexWhere((p) => p.id == existingPlan.id); + if (index != -1) updatedList[index] = newPlan; + } else { + updatedList.add(newPlan); + } - await ref.read(userProfileProvider.notifier).updateProfile( - user.copyWith(teachingPlans: updatedList), - ); + await ref.read(userProfileProvider.notifier).updateProfile( + user.copyWith(teachingPlans: updatedList), + ); - if (mounted) Navigator.pop(context); + // Trigger notification for new teaching plans + if (existingPlan == null) { + NotificationService().showTeachingPlanNotification( + teacherName: user.name ?? 'Husband', + ); + } + + if (mounted) Navigator.pop(context); }, child: const Text('Save'), ), @@ -193,10 +208,11 @@ class _HusbandDevotionalScreenState extends ConsumerState p.id != plan.id).toList(); + final updatedList = + user.teachingPlans!.where((p) => p.id != plan.id).toList(); await ref.read(userProfileProvider.notifier).updateProfile( - user.copyWith(teachingPlans: updatedList), - ); + user.copyWith(teachingPlans: updatedList), + ); } void _toggleComplete(TeachingPlan plan) async { @@ -207,17 +223,17 @@ class _HusbandDevotionalScreenState extends ConsumerState a.date.compareTo(b.date)); + upcomingPlans.sort((a, b) => a.date.compareTo(b.date)); // Listen for translation changes to re-fetch ref.listen(userProfileProvider, (prev, next) { @@ -253,12 +269,13 @@ class _HusbandDevotionalScreenState extends ConsumerState _showAddTeachingDialog(), - icon: const Icon(Icons.add_circle, color: AppColors.navyBlue, size: 28), + icon: const Icon(Icons.add_circle, + color: AppColors.navyBlue, size: 28), ), ], ), const SizedBox(height: 12), - + if (upcomingPlans.isEmpty) Container( width: double.infinity, @@ -303,39 +320,48 @@ class _HusbandDevotionalScreenState extends ConsumerState _deletePlan(plan), child: Card( elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), child: ListTile( onTap: () => _showAddTeachingDialog(plan), leading: IconButton( - icon: Icon( - plan.isCompleted ? Icons.check_circle : Icons.circle_outlined, - color: plan.isCompleted ? Colors.green : Colors.grey - ), - onPressed: () => _toggleComplete(plan), + icon: Icon( + plan.isCompleted + ? Icons.check_circle + : Icons.circle_outlined, + color: plan.isCompleted + ? Colors.green + : Colors.grey), + onPressed: () => _toggleComplete(plan), ), title: Text( plan.topic, style: GoogleFonts.outfit( fontWeight: FontWeight.w600, - decoration: plan.isCompleted ? TextDecoration.lineThrough : null, + decoration: plan.isCompleted + ? TextDecoration.lineThrough + : null, ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (plan.scriptureReference.isNotEmpty) - Text(plan.scriptureReference, style: const TextStyle(fontWeight: FontWeight.w500)), + Text(plan.scriptureReference, + style: const TextStyle( + fontWeight: FontWeight.w500)), if (plan.notes.isNotEmpty) Text( - plan.notes, - maxLines: 2, + plan.notes, + maxLines: 2, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 4), - Text( - DateFormat.yMMMd().format(plan.date), - style: TextStyle(fontSize: 11, color: Colors.grey[600]), - ), + const SizedBox(height: 4), + Text( + DateFormat.yMMMd().format(plan.date), + style: TextStyle( + fontSize: 11, color: Colors.grey[600]), + ), ], ), isThreeLine: true, @@ -344,8 +370,13 @@ class _HusbandDevotionalScreenState extends ConsumerState 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'), + ), + ], + ), + ); + } } diff --git a/lib/screens/husband/husband_home_screen.dart b/lib/screens/husband/husband_home_screen.dart index 114922e..6f504b9 100644 --- a/lib/screens/husband/husband_home_screen.dart +++ b/lib/screens/husband/husband_home_screen.dart @@ -1,18 +1,21 @@ -import 'package:christian_period_tracker/models/user_profile.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../../theme/app_theme.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../models/user_profile.dart'; +import '../../models/teaching_plan.dart'; import '../../models/cycle_entry.dart'; import '../../models/scripture.dart'; import '../../providers/user_provider.dart'; -import '../../services/cycle_service.dart'; -import '../../services/mock_data_service.dart'; // Import mock service -import '../calendar/calendar_screen.dart'; // Import calendar -import 'husband_notes_screen.dart'; // Import notes screen -import 'learn_article_screen.dart'; // Import learn article screen +import '../../theme/app_theme.dart'; +import '../../services/notification_service.dart'; +import '../../services/mock_data_service.dart'; +import '../../services/bible_xml_parser.dart'; +import '../calendar/calendar_screen.dart'; import 'husband_devotional_screen.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'husband_settings_screen.dart'; +import 'husband_appearance_screen.dart'; +import 'learn_article_screen.dart'; +import 'husband_notes_screen.dart'; /// Husband's companion app main screen class HusbandHomeScreen extends ConsumerStatefulWidget { @@ -27,71 +30,77 @@ class _HusbandHomeScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final husbandTheme = _husbandTheme; + final isDark = husbandTheme.brightness == Brightness.dark; + return Theme( - data: _husbandTheme, - child: Scaffold( - backgroundColor: AppColors.warmCream, - body: IndexedStack( - index: _selectedIndex, - children: [ - const _HusbandDashboard(), - const CalendarScreen(readOnly: true), // Reused Calendar - const HusbandDevotionalScreen(), // Devotional & Planning - const _HusbandTipsScreen(), - const _HusbandLearnScreen(), - const HusbandSettingsScreen(), - ], - ), - bottomNavigationBar: Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: AppColors.navyBlue.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, -2), - ), + data: husbandTheme, + child: Builder( + builder: (context) => Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: IndexedStack( + index: _selectedIndex, + children: [ + const _HusbandDashboard(), + const CalendarScreen(readOnly: true), // Reused Calendar + const HusbandDevotionalScreen(), // Devotional & Planning + const _HusbandTipsScreen(), + const _HusbandLearnScreen(), + const HusbandSettingsScreen(), ], ), - child: BottomNavigationBar( - currentIndex: _selectedIndex, - onTap: (index) => setState(() => _selectedIndex = index), - backgroundColor: Colors.white, - selectedItemColor: AppColors.navyBlue, - unselectedItemColor: AppColors.warmGray, - type: BottomNavigationBarType.fixed, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home_outlined), - activeIcon: Icon(Icons.home), - label: 'Home', - ), - BottomNavigationBarItem( - icon: Icon(Icons.calendar_month_outlined), - activeIcon: Icon(Icons.calendar_month), - label: 'Calendar', - ), - BottomNavigationBarItem( - icon: Icon(Icons.menu_book_outlined), - activeIcon: Icon(Icons.menu_book), - label: 'Devotion', - ), - BottomNavigationBarItem( - icon: Icon(Icons.lightbulb_outline), - activeIcon: Icon(Icons.lightbulb), - label: 'Tips', - ), - BottomNavigationBarItem( - icon: Icon(Icons.school_outlined), - activeIcon: Icon(Icons.school), - label: 'Learn', - ), - BottomNavigationBarItem( - icon: Icon(Icons.settings_outlined), - activeIcon: Icon(Icons.settings), - label: 'Settings', - ), - ], + bottomNavigationBar: Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E1E1E) : Colors.white, + boxShadow: [ + BoxShadow( + color: (isDark ? Colors.black : AppColors.navyBlue) + .withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: BottomNavigationBar( + currentIndex: _selectedIndex, + onTap: (index) => setState(() => _selectedIndex = index), + backgroundColor: isDark ? const Color(0xFF1E1E1E) : Colors.white, + selectedItemColor: Theme.of(context).colorScheme.primary, + unselectedItemColor: isDark ? Colors.grey : AppColors.warmGray, + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home_outlined), + activeIcon: Icon(Icons.home), + label: 'Home', + ), + BottomNavigationBarItem( + icon: Icon(Icons.calendar_month_outlined), + activeIcon: Icon(Icons.calendar_month), + label: 'Calendar', + ), + BottomNavigationBarItem( + icon: Icon(Icons.menu_book_outlined), + activeIcon: Icon(Icons.menu_book), + label: 'Devotion', + ), + BottomNavigationBarItem( + icon: Icon(Icons.lightbulb_outline), + activeIcon: Icon(Icons.lightbulb), + label: 'Tips', + ), + BottomNavigationBarItem( + icon: Icon(Icons.school_outlined), + activeIcon: Icon(Icons.school), + label: 'Learn', + ), + BottomNavigationBarItem( + icon: Icon(Icons.settings_outlined), + activeIcon: Icon(Icons.settings), + label: 'Settings', + ), + ], + ), ), ), ), @@ -99,32 +108,199 @@ class _HusbandHomeScreenState extends ConsumerState { } ThemeData get _husbandTheme { - return ThemeData( - useMaterial3: true, - brightness: Brightness.light, - scaffoldBackgroundColor: AppColors.warmCream, - colorScheme: const ColorScheme.light( - primary: AppColors.navyBlue, - secondary: AppColors.gold, - surface: AppColors.warmCream, - ), - appBarTheme: AppBarTheme( - backgroundColor: AppColors.warmCream, - foregroundColor: AppColors.navyBlue, - elevation: 0, - titleTextStyle: GoogleFonts.outfit( - fontSize: 20, - fontWeight: FontWeight.w600, - color: AppColors.navyBlue, + final user = ref.watch(userProfileProvider); + final husbandThemeMode = user?.husbandThemeMode ?? AppThemeMode.system; + final husbandAccentColor = user?.husbandAccentColor ?? '0xFF1A3A5C'; + + // Determine brightness based on husbandThemeMode + Brightness brightness = MediaQuery.platformBrightnessOf(context); + if (husbandThemeMode == AppThemeMode.light) { + brightness = Brightness.light; + } else if (husbandThemeMode == AppThemeMode.dark) { + brightness = Brightness.dark; + } + + final isDark = brightness == Brightness.dark; + final accentColor = Color(int.parse(husbandAccentColor)); + + // Dark theme with good contrast + if (isDark) { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF121212), + colorScheme: ColorScheme.dark( + primary: accentColor, + secondary: const Color(0xFFD4A574), // Warm gold for contrast + surface: const Color(0xFF1E1E1E), + onSurface: Colors.white, + onBackground: Colors.white, ), - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.navyBlue, + // Text theme with high contrast for dark mode + textTheme: TextTheme( + headlineLarge: GoogleFonts.outfit( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + headlineMedium: GoogleFonts.outfit( + fontSize: 22, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + titleLarge: GoogleFonts.outfit( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + titleMedium: GoogleFonts.outfit( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white.withOpacity(0.95), + ), + bodyLarge: GoogleFonts.outfit( + fontSize: 16, + color: Colors.white.withOpacity(0.9), + ), + bodyMedium: GoogleFonts.outfit( + fontSize: 14, + color: Colors.white.withOpacity(0.85), + ), + bodySmall: GoogleFonts.outfit( + fontSize: 12, + color: Colors.white.withOpacity(0.7), + ), + labelLarge: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + appBarTheme: AppBarTheme( + backgroundColor: const Color(0xFF1E1E1E), foregroundColor: Colors.white, + elevation: 0, + iconTheme: const IconThemeData(color: Colors.white), + titleTextStyle: GoogleFonts.outfit( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Colors.white, + ), ), - ), - ); + cardTheme: CardThemeData( + color: const Color(0xFF2A2A2A), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + cardColor: const Color(0xFF2A2A2A), + dividerColor: Colors.white.withOpacity(0.12), + iconTheme: const IconThemeData(color: Colors.white70), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: accentColor, + foregroundColor: Colors.white, + ), + ), + segmentedButtonTheme: SegmentedButtonThemeData( + style: SegmentedButton.styleFrom( + backgroundColor: const Color(0xFF2A2A2A), + foregroundColor: Colors.white70, + selectedBackgroundColor: accentColor, + selectedForegroundColor: Colors.white, + ), + ), + ); + } else { + // Light theme + return ThemeData( + useMaterial3: true, + brightness: Brightness.light, + scaffoldBackgroundColor: AppColors.warmCream, + colorScheme: ColorScheme.light( + primary: accentColor, + secondary: AppColors.gold, + surface: Colors.white, + onSurface: AppColors.charcoal, + onBackground: AppColors.charcoal, + ), + textTheme: TextTheme( + headlineLarge: GoogleFonts.outfit( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppColors.charcoal, + ), + headlineMedium: GoogleFonts.outfit( + fontSize: 22, + fontWeight: FontWeight.w600, + color: AppColors.charcoal, + ), + titleLarge: GoogleFonts.outfit( + fontSize: 18, + fontWeight: FontWeight.w600, + color: AppColors.charcoal, + ), + titleMedium: GoogleFonts.outfit( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColors.charcoal.withOpacity(0.9), + ), + bodyLarge: GoogleFonts.outfit( + fontSize: 16, + color: AppColors.charcoal, + ), + bodyMedium: GoogleFonts.outfit( + fontSize: 14, + color: AppColors.charcoal.withOpacity(0.85), + ), + bodySmall: GoogleFonts.outfit( + fontSize: 12, + color: AppColors.warmGray, + ), + labelLarge: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.charcoal, + ), + ), + appBarTheme: AppBarTheme( + backgroundColor: AppColors.warmCream, + foregroundColor: accentColor, + elevation: 0, + iconTheme: IconThemeData(color: accentColor), + titleTextStyle: GoogleFonts.outfit( + fontSize: 20, + fontWeight: FontWeight.w600, + color: accentColor, + ), + ), + cardTheme: CardThemeData( + color: Colors.white, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + cardColor: Colors.white, + dividerColor: AppColors.lightGray, + iconTheme: IconThemeData(color: AppColors.warmGray), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: accentColor, + foregroundColor: Colors.white, + ), + ), + segmentedButtonTheme: SegmentedButtonThemeData( + style: SegmentedButton.styleFrom( + backgroundColor: AppColors.lightGray, + foregroundColor: AppColors.charcoal, + selectedBackgroundColor: accentColor, + selectedForegroundColor: Colors.white, + ), + ), + ); + } } } @@ -160,7 +336,8 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { final dayOfCycle = cycleInfo.dayOfCycle; final daysUntilPeriod = cycleInfo.daysUntilPeriod; - final scripture = _currentScripture ?? ScriptureDatabase().getHusbandScripture(); + final scripture = + _currentScripture ?? ScriptureDatabase().getHusbandScripture(); return SafeArea( child: SingleChildScrollView( @@ -340,81 +517,87 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { // Recent Cravings (Dynamic) Builder( builder: (context) { - // Get recent cravings from the last 3 days - final allEntries = ref.read(cycleEntriesProvider); - // Sort by date desc - final sortedEntries = List.from(allEntries)..sort((a,b) => b.date.compareTo(a.date)); - - final recentCravings = {}; - final now = DateTime.now(); - for (var entry in sortedEntries) { - if (now.difference(entry.date).inDays > 3) break; - if (entry.cravings != null) { - recentCravings.addAll(entry.cravings!); - } - } + // Get recent cravings from the last 3 days + final allEntries = ref.read(cycleEntriesProvider); + // Sort by date desc + final sortedEntries = List.from(allEntries) + ..sort((a, b) => b.date.compareTo(a.date)); - if (recentCravings.isEmpty) return const SizedBox.shrink(); + final recentCravings = {}; + final now = DateTime.now(); + for (var entry in sortedEntries) { + if (now.difference(entry.date).inDays > 3) break; + if (entry.cravings != null) { + recentCravings.addAll(entry.cravings!); + } + } - return Container( - width: double.infinity, - margin: const EdgeInsets.only(bottom: 20), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.rose.withOpacity(0.3)), - boxShadow: [ - BoxShadow( - color: AppColors.rose.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: AppColors.rose.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.fastfood, - color: AppColors.rose, - size: 20, - ), + if (recentCravings.isEmpty) return const SizedBox.shrink(); + + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.rose.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: AppColors.rose.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.rose.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), ), - const SizedBox(width: 12), - Text( - 'She is Craving...', - style: GoogleFonts.outfit( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.navyBlue, - ), + child: Icon( + Icons.fastfood, + color: AppColors.rose, + size: 20, ), - ], - ), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: recentCravings.map((craving) => Chip( - label: Text(craving), - backgroundColor: AppColors.rose.withOpacity(0.1), - labelStyle: GoogleFonts.outfit(color: AppColors.navyBlue, fontWeight: FontWeight.w500), - side: BorderSide.none, - )).toList(), - ), - ], - ), - ); + ), + const SizedBox(width: 12), + Text( + 'She is Craving...', + style: GoogleFonts.outfit( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.navyBlue, + ), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: recentCravings + .map((craving) => Chip( + label: Text(craving), + backgroundColor: + AppColors.rose.withOpacity(0.1), + labelStyle: GoogleFonts.outfit( + color: AppColors.navyBlue, + fontWeight: FontWeight.w500), + side: BorderSide.none, + )) + .toList(), + ), + ], + ), + ); }, ), @@ -461,7 +644,8 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { GestureDetector( onTap: () => _showVersionPicker(context, ref), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), decoration: BoxDecoration( color: AppColors.gold.withOpacity(0.15), borderRadius: BorderRadius.circular(12), @@ -478,7 +662,8 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { ), ), const SizedBox(width: 2), - Icon(Icons.arrow_drop_down, color: AppColors.gold, size: 16), + Icon(Icons.arrow_drop_down, + color: AppColors.gold, size: 16), ], ), ), @@ -510,7 +695,8 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { GestureDetector( onTap: _loadNewVerse, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), decoration: BoxDecoration( color: AppColors.gold.withOpacity(0.1), borderRadius: BorderRadius.circular(8), @@ -518,7 +704,8 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.refresh, color: AppColors.gold, size: 14), + Icon(Icons.refresh, + color: AppColors.gold, size: 14), const SizedBox(width: 4), Text( 'New Verse', @@ -615,23 +802,26 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { ), const SizedBox(height: 16), ...BibleTranslation.values.map((translation) => ListTile( - title: Text( - translation.label, - style: GoogleFonts.outfit(fontWeight: FontWeight.w500), - ), - trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation - ? const Icon(Icons.check, color: AppColors.sageGreen) - : null, - onTap: () async { - final profile = ref.read(userProfileProvider); - if (profile != null) { - await ref.read(userProfileProvider.notifier).updateProfile( - profile.copyWith(bibleTranslation: translation), - ); - } - if (context.mounted) Navigator.pop(context); - }, - )), + title: Text( + translation.label, + style: GoogleFonts.outfit(fontWeight: FontWeight.w500), + ), + trailing: ref.watch(userProfileProvider)?.bibleTranslation == + translation + ? const Icon(Icons.check, color: AppColors.sageGreen) + : null, + onTap: () async { + final profile = ref.read(userProfileProvider); + if (profile != null) { + await ref + .read(userProfileProvider.notifier) + .updateProfile( + profile.copyWith(bibleTranslation: translation), + ); + } + if (context.mounted) Navigator.pop(context); + }, + )), ], ), ), @@ -680,6 +870,29 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> { child: const Text('Amen'), ), ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: TextButton.icon( + onPressed: () { + final user = ref.read(userProfileProvider); + NotificationService().showPartnerUpdateNotification( + title: 'Your husband prayed for you!', + body: + '${user?.name ?? 'He'} just finished praying for you during her ${phase.label.toLowerCase()} phase.', + ); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Sent to wife!')), + ); + }, + icon: const Icon(Icons.share, size: 18), + label: const Text('Share with Wife'), + style: TextButton.styleFrom( + foregroundColor: AppColors.navyBlue, + ), + ), + ), const SizedBox(height: 16), ], ), @@ -744,133 +957,146 @@ class _HusbandTipsScreen extends StatelessWidget { '🙏 Pray for her physical comfort', ]), const SizedBox(height: 16), - + // Period Supplies (Dynamic) Consumer( builder: (context, ref, child) { - final user = ref.watch(userProfileProvider); - if (user == null || !user.isPadTrackingEnabled) return const SizedBox.shrink(); - - final brand = user.padBrand ?? 'Not specified'; - final flow = user.typicalFlowIntensity; - - return Column( - children: [ - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.menstrualPhase.withOpacity(0.15), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.shopping_bag_outlined, color: AppColors.menstrualPhase, size: 20), - ), - const SizedBox(width: 12), - Text( - 'Period Supplies', - style: GoogleFonts.outfit( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.navyBlue, - ), - ), - ], - ), - const SizedBox(height: 12), - Text( - 'She uses:', - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), - ), - const SizedBox(height: 4), - Text( - brand, - style: GoogleFonts.outfit(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.navyBlue), - ), - if (flow != null) ...[ - const SizedBox(height: 8), - Text( - 'Typical Flow: $flow/5', - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal), - ), - ], - if (user.padAbsorbency != null) ...[ - const SizedBox(height: 4), - Text( - 'Absorbency: ${user.padAbsorbency}/5', - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal), - ), - ], - - // Low Stock Warning - if (user.padInventoryCount <= user.lowInventoryThreshold) ...[ - const SizedBox(height: 12), - Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColors.rose.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.rose), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning_amber_rounded, color: AppColors.rose, size: 20), - const SizedBox(width: 8), - Text( - 'LOW STOCK! (${user.padInventoryCount} left)', - style: GoogleFonts.outfit( - fontSize: 14, - fontWeight: FontWeight.bold, - color: AppColors.rose - ), - ), - ], - ), - const SizedBox(height: 4), - GestureDetector( - onTap: () { - // Navigate to settings - final parentState = context.findAncestorStateOfType<_HusbandHomeScreenState>(); - if (parentState != null) { - parentState.setState(() { - parentState._selectedIndex = 5; // Settings tab - }); - } - }, - child: Text( - 'Check Settings to Sync', - style: GoogleFonts.outfit( - fontSize: 12, - decoration: TextDecoration.underline, - color: AppColors.rose, - ), - ), - ), - ], - ), - ), - ], - ], - ), + final user = ref.watch(userProfileProvider); + if (user == null || !user.isPadTrackingEnabled) + return const SizedBox.shrink(); + + final brand = user.padBrand ?? 'Not specified'; + final flow = user.typicalFlowIntensity; + + return Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.menstrualPhase.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColors.menstrualPhase.withOpacity(0.3)), ), - const SizedBox(height: 16), - ], - ); + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.shopping_bag_outlined, + color: AppColors.menstrualPhase, size: 20), + ), + const SizedBox(width: 12), + Text( + 'Period Supplies', + style: GoogleFonts.outfit( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.navyBlue, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + 'She uses:', + style: GoogleFonts.outfit( + fontSize: 14, color: AppColors.warmGray), + ), + const SizedBox(height: 4), + Text( + brand, + style: GoogleFonts.outfit( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.navyBlue), + ), + if (flow != null) ...[ + const SizedBox(height: 8), + Text( + 'Typical Flow: $flow/5', + style: GoogleFonts.outfit( + fontSize: 14, color: AppColors.charcoal), + ), + ], + if (user.padAbsorbency != null) ...[ + const SizedBox(height: 4), + Text( + 'Absorbency: ${user.padAbsorbency}/5', + style: GoogleFonts.outfit( + fontSize: 14, color: AppColors.charcoal), + ), + ], + + // Low Stock Warning + if (user.padInventoryCount <= + user.lowInventoryThreshold) ...[ + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.rose.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.rose), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.warning_amber_rounded, + color: AppColors.rose, size: 20), + const SizedBox(width: 8), + Text( + 'LOW STOCK! (${user.padInventoryCount} left)', + style: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.rose), + ), + ], + ), + const SizedBox(height: 4), + GestureDetector( + onTap: () { + // Navigate to settings + final parentState = + context.findAncestorStateOfType< + _HusbandHomeScreenState>(); + if (parentState != null) { + parentState.setState(() { + parentState._selectedIndex = + 5; // Settings tab + }); + } + }, + child: Text( + 'Check Settings to Sync', + style: GoogleFonts.outfit( + fontSize: 12, + decoration: TextDecoration.underline, + color: AppColors.rose, + ), + ), + ), + ], + ), + ), + ], + ], + ), + ), + const SizedBox(height: 16), + ], + ); }, ), @@ -889,21 +1115,23 @@ class _HusbandTipsScreen extends StatelessWidget { ]), const SizedBox(height: 16), const SizedBox(height: 16), - + // Her Favorites Section Consumer( builder: (context, ref, child) { - final user = ref.watch(userProfileProvider); - final favorites = user?.favoriteFoods; + final user = ref.watch(userProfileProvider); + final favorites = user?.favoriteFoods; - if (favorites == null || favorites.isEmpty) return const SizedBox.shrink(); + if (favorites == null || favorites.isEmpty) + return const SizedBox.shrink(); - return Column( - children: [ - _buildTipCategory('❤️ Her Favorites (Cheat Sheet)', favorites), - const SizedBox(height: 16), - ], - ); + return Column( + children: [ + _buildTipCategory( + '❤️ Her Favorites (Cheat Sheet)', favorites), + const SizedBox(height: 16), + ], + ); }, ), @@ -1046,7 +1274,8 @@ class _HusbandLearnScreen extends StatelessWidget { ); } - Widget _buildSection(BuildContext context, String title, List<_LearnItem> items) { + Widget _buildSection( + BuildContext context, String title, List<_LearnItem> items) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1104,7 +1333,8 @@ class _HusbandLearnScreen extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => LearnArticleScreen(articleId: item.articleId), + builder: (context) => + LearnArticleScreen(articleId: item.articleId), ), ); }, @@ -1196,14 +1426,16 @@ class _HusbandSettingsScreen extends ConsumerWidget { // Need to preserve current Husband ID and Role but take other data final currentProfile = ref.read(userProfileProvider); if (currentProfile != null) { - final updatedProfile = currentProfile.copyWith( - partnerName: mockWife.name, - averageCycleLength: mockWife.averageCycleLength, - averagePeriodLength: mockWife.averagePeriodLength, - lastPeriodStartDate: mockWife.lastPeriodStartDate, - favoriteFoods: mockWife.favoriteFoods, - ); - await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); + final updatedProfile = currentProfile.copyWith( + partnerName: mockWife.name, + averageCycleLength: mockWife.averageCycleLength, + averagePeriodLength: mockWife.averagePeriodLength, + lastPeriodStartDate: mockWife.lastPeriodStartDate, + favoriteFoods: mockWife.favoriteFoods, + ); + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); } } } @@ -1230,23 +1462,26 @@ class _HusbandSettingsScreen extends ConsumerWidget { ), const SizedBox(height: 16), ...BibleTranslation.values.map((translation) => ListTile( - title: Text( - translation.label, - style: GoogleFonts.outfit(fontWeight: FontWeight.w500), - ), - trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation - ? const Icon(Icons.check, color: AppColors.sageGreen) - : null, - onTap: () async { - final profile = ref.read(userProfileProvider); - if (profile != null) { - await ref.read(userProfileProvider.notifier).updateProfile( - profile.copyWith(bibleTranslation: translation), - ); - } - if (context.mounted) Navigator.pop(context); - }, - )), + title: Text( + translation.label, + style: GoogleFonts.outfit(fontWeight: FontWeight.w500), + ), + trailing: ref.watch(userProfileProvider)?.bibleTranslation == + translation + ? const Icon(Icons.check, color: AppColors.sageGreen) + : null, + onTap: () async { + final profile = ref.read(userProfileProvider); + if (profile != null) { + await ref + .read(userProfileProvider.notifier) + .updateProfile( + profile.copyWith(bibleTranslation: translation), + ); + } + if (context.mounted) Navigator.pop(context); + }, + )), ], ), ), @@ -1256,105 +1491,116 @@ class _HusbandSettingsScreen extends ConsumerWidget { void _showConnectDialog(BuildContext context, WidgetRef ref) { final codeController = TextEditingController(); bool shareDevotional = true; - + showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setState) => AlertDialog( - title: Row( - children: [ - const Icon(Icons.link, color: AppColors.navyBlue), - const SizedBox(width: 8), - const Text('Connect with Wife'), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Enter the pairing code from your wife\'s app:', - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), - ), - const SizedBox(height: 16), - TextField( - controller: codeController, - decoration: const InputDecoration( - hintText: 'e.g., ABC123', - border: OutlineInputBorder(), + title: Row( + children: [ + const Icon(Icons.link, color: AppColors.navyBlue), + const SizedBox(width: 8), + const Text('Connect with Wife'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Enter the pairing code from your wife\'s app:', + style: + GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), ), - textCapitalization: TextCapitalization.characters, - ), - const SizedBox(height: 16), - Text( - 'Your wife can find this code in her Settings under "Share with Husband".', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - ), - const SizedBox(height: 24), - Row( + const SizedBox(height: 16), + TextField( + controller: codeController, + decoration: const InputDecoration( + hintText: 'e.g., ABC123', + border: OutlineInputBorder(), + ), + textCapitalization: TextCapitalization.characters, + ), + const SizedBox(height: 16), + Text( + 'Your wife can find this code in her Settings under "Share with Husband".', + style: + GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), + ), + const SizedBox(height: 24), + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - height: 24, - width: 24, - child: Checkbox( - value: shareDevotional, - onChanged: (val) => setState(() => shareDevotional = val ?? true), - activeColor: AppColors.navyBlue, - ), + SizedBox( + height: 24, + width: 24, + child: Checkbox( + value: shareDevotional, + onChanged: (val) => + setState(() => shareDevotional = val ?? true), + activeColor: AppColors.navyBlue, ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Share Devotional Plans', - style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.charcoal), - ), - Text( - 'Allow her to see the teaching plans you create.', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - ), - ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Share Devotional Plans', + style: GoogleFonts.outfit( + fontWeight: FontWeight.bold, + fontSize: 14, + color: AppColors.charcoal), ), + Text( + 'Allow her to see the teaching plans you create.', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray), + ), + ], ), + ), ], ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + ], ), - ElevatedButton( - onPressed: () async { - final code = codeController.text.trim(); - - Navigator.pop(context); - - // Update preference - final user = ref.read(userProfileProvider); - if (user != null) { - await ref.read(userProfileProvider.notifier).updateProfile( - user.copyWith(isDataShared: shareDevotional) - ); - } + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () async { + final code = codeController.text.trim(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Settings updated & Connected!'), - backgroundColor: AppColors.sageGreen, - ), - ); - - if (code.isNotEmpty) { + Navigator.pop(context); + + // Update preference + final user = ref.read(userProfileProvider); + if (user != null) { + final updatedProfile = + user.copyWith(isDataShared: shareDevotional); + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Settings updated & Connected!'), + backgroundColor: AppColors.sageGreen, + ), + ); + + if (code.isNotEmpty) { // Load demo data as simulation final mockService = MockDataService(); final entries = mockService.generateMockCycleEntries(); for (var entry in entries) { - await ref.read(cycleEntriesProvider.notifier).addEntry(entry); + await ref + .read(cycleEntriesProvider.notifier) + .addEntry(entry); } final mockWife = mockService.generateMockWifeProfile(); final currentProfile = ref.read(userProfileProvider); @@ -1367,18 +1613,20 @@ class _HusbandSettingsScreen extends ConsumerWidget { lastPeriodStartDate: mockWife.lastPeriodStartDate, favoriteFoods: mockWife.favoriteFoods, ); - await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.navyBlue, - foregroundColor: Colors.white, + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.navyBlue, + foregroundColor: Colors.white, + ), + child: const Text('Connect'), ), - child: const Text('Connect'), - ), - ], - ), + ], + ), ), ); } @@ -1416,8 +1664,7 @@ class _HusbandSettingsScreen extends ConsumerWidget { ), const Divider(height: 1), ListTile( - leading: const Icon(Icons.link, - color: AppColors.navyBlue), + leading: const Icon(Icons.link, color: AppColors.navyBlue), title: Text('Connect with Wife', style: GoogleFonts.outfit(fontWeight: FontWeight.w500)), trailing: const Icon(Icons.chevron_right), @@ -1433,7 +1680,9 @@ class _HusbandSettingsScreen extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? 'ESV', + ref.watch(userProfileProvider + .select((u) => u?.bibleTranslation.label)) ?? + 'ESV', style: GoogleFonts.outfit( fontSize: 14, color: AppColors.warmGray, @@ -1452,10 +1701,10 @@ class _HusbandSettingsScreen extends ConsumerWidget { style: GoogleFonts.outfit(fontWeight: FontWeight.w500)), trailing: const Icon(Icons.chevron_right), onTap: () { - Navigator.push( + Navigator.push( context, MaterialPageRoute( - builder: (context) => const AppearanceScreen(), + builder: (context) => const HusbandAppearanceScreen(), ), ); }, @@ -1685,12 +1934,14 @@ class _HusbandWifeStatus extends ConsumerWidget { const SizedBox(width: 12), Text( '$label: ', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, fontSize: 14), + style: + GoogleFonts.outfit(fontWeight: FontWeight.w500, fontSize: 14), ), Expanded( child: Text( value, - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal), + style: + GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal), ), ), ], @@ -1715,7 +1966,8 @@ class _HusbandWifeStatus extends ConsumerWidget { Expanded( child: Text( text, - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.navyBlue), + style: + GoogleFonts.outfit(fontSize: 14, color: AppColors.navyBlue), ), ), ], @@ -1777,4 +2029,3 @@ class _HusbandWifeStatus extends ConsumerWidget { return list.take(4).toList(); } } - diff --git a/lib/screens/husband/husband_settings_screen.dart b/lib/screens/husband/husband_settings_screen.dart index 21942db..e46fafd 100644 --- a/lib/screens/husband/husband_settings_screen.dart +++ b/lib/screens/husband/husband_settings_screen.dart @@ -5,8 +5,7 @@ import '../../theme/app_theme.dart'; import '../../models/user_profile.dart'; import '../../providers/user_provider.dart'; import '../../services/mock_data_service.dart'; -import '../../providers/cycle_provider.dart'; // Ensure cycleEntriesProvider is available -import '../settings/appearance_screen.dart'; +import 'husband_appearance_screen.dart'; class HusbandSettingsScreen extends ConsumerWidget { const HusbandSettingsScreen({super.key}); @@ -72,20 +71,22 @@ class HusbandSettingsScreen extends ConsumerWidget { final mockWife = mockService.generateMockWifeProfile(); final currentProfile = ref.read(userProfileProvider); if (currentProfile != null) { - final updatedProfile = currentProfile.copyWith( - partnerName: mockWife.name, - averageCycleLength: mockWife.averageCycleLength, - averagePeriodLength: mockWife.averagePeriodLength, - lastPeriodStartDate: mockWife.lastPeriodStartDate, - favoriteFoods: mockWife.favoriteFoods, - ); - await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); + final updatedProfile = currentProfile.copyWith( + partnerName: mockWife.name, + averageCycleLength: mockWife.averageCycleLength, + averagePeriodLength: mockWife.averagePeriodLength, + lastPeriodStartDate: mockWife.lastPeriodStartDate, + favoriteFoods: mockWife.favoriteFoods, + ); + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); } - + if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Demo data loaded')), - ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Demo data loaded')), + ); } } } @@ -112,23 +113,26 @@ class HusbandSettingsScreen extends ConsumerWidget { ), const SizedBox(height: 16), ...BibleTranslation.values.map((translation) => ListTile( - title: Text( - translation.label, - style: GoogleFonts.outfit(fontWeight: FontWeight.w500), - ), - trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation - ? const Icon(Icons.check, color: AppColors.sageGreen) - : null, - onTap: () async { - final profile = ref.read(userProfileProvider); - if (profile != null) { - await ref.read(userProfileProvider.notifier).updateProfile( - profile.copyWith(bibleTranslation: translation), - ); - } - if (context.mounted) Navigator.pop(context); - }, - )), + title: Text( + translation.label, + style: GoogleFonts.outfit(fontWeight: FontWeight.w500), + ), + trailing: ref.watch(userProfileProvider)?.bibleTranslation == + translation + ? const Icon(Icons.check, color: AppColors.sageGreen) + : null, + onTap: () async { + final profile = ref.read(userProfileProvider); + if (profile != null) { + await ref + .read(userProfileProvider.notifier) + .updateProfile( + profile.copyWith(bibleTranslation: translation), + ); + } + if (context.mounted) Navigator.pop(context); + }, + )), ], ), ), @@ -138,104 +142,112 @@ class HusbandSettingsScreen extends ConsumerWidget { void _showConnectDialog(BuildContext context, WidgetRef ref) { final codeController = TextEditingController(); bool shareDevotional = true; - + showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setState) => AlertDialog( - title: Row( - children: [ - const Icon(Icons.link, color: AppColors.navyBlue), - const SizedBox(width: 8), - const Text('Connect with Wife'), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Enter the pairing code from your wife\'s app:', - style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), - ), - const SizedBox(height: 16), - TextField( - controller: codeController, - decoration: const InputDecoration( - hintText: 'e.g., ABC123', - border: OutlineInputBorder(), + title: Row( + children: [ + const Icon(Icons.link, color: AppColors.navyBlue), + const SizedBox(width: 8), + const Text('Connect with Wife'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Enter the pairing code from your wife\'s app:', + style: + GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray), ), - textCapitalization: TextCapitalization.characters, - ), - const SizedBox(height: 16), - Text( - 'Your wife can find this code in her Settings under "Share with Husband".', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - ), - const SizedBox(height: 24), - Row( + const SizedBox(height: 16), + TextField( + controller: codeController, + decoration: const InputDecoration( + hintText: 'e.g., ABC123', + border: OutlineInputBorder(), + ), + textCapitalization: TextCapitalization.characters, + ), + const SizedBox(height: 16), + Text( + 'Your wife can find this code in her Settings under "Share with Husband".', + style: + GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), + ), + const SizedBox(height: 24), + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - height: 24, - width: 24, - child: Checkbox( - value: shareDevotional, - onChanged: (val) => setState(() => shareDevotional = val ?? true), - activeColor: AppColors.navyBlue, - ), + SizedBox( + height: 24, + width: 24, + child: Checkbox( + value: shareDevotional, + onChanged: (val) => + setState(() => shareDevotional = val ?? true), + activeColor: AppColors.navyBlue, ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Share Devotional Plans', - style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.charcoal), - ), - Text( - 'Allow her to see the teaching plans you create.', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - ), - ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Share Devotional Plans', + style: GoogleFonts.outfit( + fontWeight: FontWeight.bold, + fontSize: 14, + color: AppColors.charcoal), ), + Text( + 'Allow her to see the teaching plans you create.', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray), + ), + ], ), + ), ], ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + ], ), - ElevatedButton( - onPressed: () async { - final code = codeController.text.trim(); - - Navigator.pop(context); - - // Update preference - final user = ref.read(userProfileProvider); - if (user != null) { - await ref.read(userProfileProvider.notifier).updateProfile( - user.copyWith(isDataShared: shareDevotional) - ); - } + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () async { + final code = codeController.text.trim(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Settings updated & Connected!'), - backgroundColor: AppColors.sageGreen, - ), - ); - - if (code.isNotEmpty) { + Navigator.pop(context); + + // Update preference + final user = ref.read(userProfileProvider); + if (user != null) { + await ref.read(userProfileProvider.notifier).updateProfile( + user.copyWith(isDataShared: shareDevotional)); + } + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Settings updated & Connected!'), + backgroundColor: AppColors.sageGreen, + ), + ); + + if (code.isNotEmpty) { // Load demo data as simulation final mockService = MockDataService(); final entries = mockService.generateMockCycleEntries(); for (var entry in entries) { - await ref.read(cycleEntriesProvider.notifier).addEntry(entry); + await ref + .read(cycleEntriesProvider.notifier) + .addEntry(entry); } final mockWife = mockService.generateMockWifeProfile(); final currentProfile = ref.read(userProfileProvider); @@ -248,18 +260,20 @@ class HusbandSettingsScreen extends ConsumerWidget { lastPeriodStartDate: mockWife.lastPeriodStartDate, favoriteFoods: mockWife.favoriteFoods, ); - await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.navyBlue, - foregroundColor: Colors.white, + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.navyBlue, + foregroundColor: Colors.white, + ), + child: const Text('Connect'), ), - child: const Text('Connect'), - ), - ], - ), + ], + ), ), ); } @@ -268,7 +282,8 @@ class HusbandSettingsScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // Theme aware colors final isDark = Theme.of(context).brightness == Brightness.dark; - final cardColor = Theme.of(context).cardTheme.color; // Using theme card color + final cardColor = + Theme.of(context).cardTheme.color; // Using theme card color final textColor = Theme.of(context).textTheme.bodyLarge?.color; return SafeArea( @@ -282,7 +297,8 @@ class HusbandSettingsScreen extends ConsumerWidget { style: GoogleFonts.outfit( fontSize: 28, fontWeight: FontWeight.w600, - color: Theme.of(context).textTheme.displayMedium?.color ?? AppColors.navyBlue, + color: Theme.of(context).textTheme.displayMedium?.color ?? + AppColors.navyBlue, ), ), const SizedBox(height: 24), @@ -297,7 +313,8 @@ class HusbandSettingsScreen extends ConsumerWidget { leading: Icon(Icons.notifications_outlined, color: Theme.of(context).colorScheme.primary), title: Text('Notifications', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)), + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, color: textColor)), trailing: Switch(value: true, onChanged: (val) {}), ), const Divider(height: 1), @@ -305,8 +322,10 @@ class HusbandSettingsScreen extends ConsumerWidget { leading: Icon(Icons.link, color: Theme.of(context).colorScheme.primary), title: Text('Connect with Wife', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)), - trailing: Icon(Icons.chevron_right, color: Theme.of(context).disabledColor), + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, color: textColor)), + trailing: Icon(Icons.chevron_right, + color: Theme.of(context).disabledColor), onTap: () => _showConnectDialog(context, ref), ), const Divider(height: 1), @@ -314,18 +333,24 @@ class HusbandSettingsScreen extends ConsumerWidget { leading: Icon(Icons.menu_book_outlined, color: Theme.of(context).colorScheme.primary), title: Text('Bible Translation', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)), + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, color: textColor)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text( - ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? 'ESV', + ref.watch(userProfileProvider + .select((u) => u?.bibleTranslation.label)) ?? + 'ESV', style: GoogleFonts.outfit( fontSize: 14, - color: Theme.of(context).textTheme.bodyMedium?.color ?? AppColors.warmGray, + color: + Theme.of(context).textTheme.bodyMedium?.color ?? + AppColors.warmGray, ), ), - Icon(Icons.chevron_right, color: Theme.of(context).disabledColor), + Icon(Icons.chevron_right, + color: Theme.of(context).disabledColor), ], ), onTap: () => _showTranslationPicker(context, ref), @@ -335,13 +360,15 @@ class HusbandSettingsScreen extends ConsumerWidget { leading: Icon(Icons.palette_outlined, color: Theme.of(context).colorScheme.primary), title: Text('Appearance', - style: GoogleFonts.outfit(fontWeight: FontWeight.w500, color: textColor)), - trailing: Icon(Icons.chevron_right, color: Theme.of(context).disabledColor), + style: GoogleFonts.outfit( + fontWeight: FontWeight.w500, color: textColor)), + trailing: Icon(Icons.chevron_right, + color: Theme.of(context).disabledColor), onTap: () { - Navigator.push( + Navigator.push( context, MaterialPageRoute( - builder: (context) => const AppearanceScreen(), + builder: (context) => const HusbandAppearanceScreen(), ), ); }, diff --git a/lib/screens/log/pad_tracker_screen.dart b/lib/screens/log/pad_tracker_screen.dart index 7e2dbf1..fcc74eb 100644 --- a/lib/screens/log/pad_tracker_screen.dart +++ b/lib/screens/log/pad_tracker_screen.dart @@ -62,7 +62,8 @@ class _PadTrackerScreenState extends ConsumerState { void _updateTimeSinceChange() { final user = ref.read(userProfileProvider); if (user?.lastPadChangeTime != null) { - _timeSinceLastChange = DateTime.now().difference(user!.lastPadChangeTime!); + _timeSinceLastChange = + DateTime.now().difference(user!.lastPadChangeTime!); } else { _timeSinceLastChange = Duration.zero; } @@ -74,9 +75,9 @@ class _PadTrackerScreenState extends ConsumerState { final lastChange = user.lastPadChangeTime; final now = DateTime.now(); - final bool changedToday = lastChange != null && - lastChange.year == now.year && - lastChange.month == now.month && + final bool changedToday = lastChange != null && + lastChange.year == now.year && + lastChange.month == now.month && lastChange.day == now.day; if (!changedToday) { @@ -84,7 +85,8 @@ class _PadTrackerScreenState extends ConsumerState { context: context, barrierDismissible: false, builder: (context) => AlertDialog( - title: Text('Track Your Change', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)), + title: Text('Track Your Change', + style: GoogleFonts.outfit(fontWeight: FontWeight.bold)), content: Text( 'When did you last change your pad/tampon?', style: GoogleFonts.outfit(), @@ -105,18 +107,19 @@ class _PadTrackerScreenState extends ConsumerState { ); if (time != null && mounted) { final now = DateTime.now(); - final selectedDate = DateTime(now.year, now.month, now.day, time.hour, time.minute); + final selectedDate = DateTime( + now.year, now.month, now.day, time.hour, time.minute); if (selectedDate.isAfter(now)) { - _updateLastChangeTime(now); + _updateLastChangeTime(now); } else { - _updateLastChangeTime(selectedDate); + _updateLastChangeTime(selectedDate); } Navigator.pop(context); } }, child: const Text('Pick Time'), ), - TextButton( + TextButton( onPressed: () { Navigator.pop(context); }, @@ -131,12 +134,14 @@ class _PadTrackerScreenState extends ConsumerState { Future _updateLastChangeTime(DateTime time) async { final user = ref.read(userProfileProvider); if (user != null) { - final updatedProfile = user.copyWith( - lastPadChangeTime: time, - ); - await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); - _updateTimeSinceChange(); - _scheduleReminders(time); + final updatedProfile = user.copyWith( + lastPadChangeTime: time, + ); + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); + _updateTimeSinceChange(); + _scheduleReminders(time); } } @@ -161,126 +166,156 @@ class _PadTrackerScreenState extends ConsumerState { final notifyTime = changeTime.subtract(const Duration(hours: 2)); if (notifyTime.isAfter(now)) { await service.scheduleNotification( - id: 200, - title: 'Upcoming Pad Change', - body: 'Recommended change in 2 hours.', - scheduledDate: notifyTime - ); + id: 200, + title: 'Upcoming Pad Change', + body: 'Recommended change in 2 hours.', + scheduledDate: notifyTime); } } - + // 1 Hour Before if (user.notifyPad1Hour) { final notifyTime = changeTime.subtract(const Duration(hours: 1)); if (notifyTime.isAfter(now)) { await service.scheduleNotification( - id: 201, - title: 'Upcoming Pad Change', - body: 'Recommended change in 1 hour.', - scheduledDate: notifyTime - ); + id: 201, + title: 'Upcoming Pad Change', + body: 'Recommended change in 1 hour.', + scheduledDate: notifyTime); } } - + // 30 Mins Before if (user.notifyPad30Mins) { final notifyTime = changeTime.subtract(const Duration(minutes: 30)); if (notifyTime.isAfter(now)) { await service.scheduleNotification( - id: 202, - title: 'Upcoming Pad Change', - body: 'Recommended change in 30 minutes.', - scheduledDate: notifyTime - ); + id: 202, + title: 'Upcoming Pad Change', + body: 'Recommended change in 30 minutes.', + scheduledDate: notifyTime); } } // Change Now if (user.notifyPadNow) { if (changeTime.isAfter(now)) { - await service.scheduleNotification( - id: 203, - title: 'Time to Change!', - body: 'It has been $hours hours since your last change.', - scheduledDate: changeTime - ); + await service.scheduleNotification( + id: 203, + title: 'Time to Change!', + body: 'It has been $hours hours since your last change.', + scheduledDate: changeTime); } } } SupplyItem? get _activeSupply { final user = ref.watch(userProfileProvider); - if (user == null || user.padSupplies == null || user.padSupplies!.isEmpty) return null; - if (_activeSupplyIndex == null || _activeSupplyIndex! >= user.padSupplies!.length) { - return user.padSupplies!.first; + if (user == null || user.padSupplies == null || user.padSupplies!.isEmpty) + return null; + if (_activeSupplyIndex == null || + _activeSupplyIndex! >= user.padSupplies!.length) { + return user.padSupplies!.first; } return user.padSupplies![_activeSupplyIndex!]; } - bool get _shouldShowMismatchWarning { - final supply = _activeSupply; - if (supply == null) return false; - - // Spotting is fine with any protection - if (_selectedFlow == FlowIntensity.spotting) return false; + bool get _shouldShowMismatchWarning { + final supply = _activeSupply; + if (supply == null) return false; - int flowValue = 1; - switch (_selectedFlow) { - case FlowIntensity.light: flowValue = 2; break; - case FlowIntensity.medium: flowValue = 3; break; - case FlowIntensity.heavy: flowValue = 5; break; - default: break; - } - - return flowValue > supply.absorbency; - } - - int get _recommendedHours { - final supply = _activeSupply; - if (supply == null) return 6; // Default - - final type = supply.type; - - if (type == PadType.menstrualCup || - type == PadType.menstrualDisc || - type == PadType.periodUnderwear) { - return 12; - } - - int baseHours; - switch (_selectedFlow) { - case FlowIntensity.heavy: - baseHours = (type == PadType.super_pad || type == PadType.overnight || type == PadType.tampon_super) - ? 4 - : 3; - break; - case FlowIntensity.medium: - baseHours = 6; - break; - case FlowIntensity.light: - baseHours = 8; - break; - case FlowIntensity.spotting: - baseHours = 10; // More generous for spotting - break; - } + // No flow is fine with any protection + if (_selectedFlow == FlowIntensity.none) return false; + // Spotting is fine with any protection + if (_selectedFlow == FlowIntensity.spotting) return false; int flowValue = 1; switch (_selectedFlow) { - case FlowIntensity.spotting: flowValue = 1; break; - case FlowIntensity.light: flowValue = 2; break; - case FlowIntensity.medium: flowValue = 3; break; - case FlowIntensity.heavy: flowValue = 5; break; + case FlowIntensity.light: + flowValue = 2; + break; + case FlowIntensity.medium: + flowValue = 3; + break; + case FlowIntensity.heavy: + flowValue = 5; + break; + default: + break; + } + + return flowValue > supply.absorbency; + } + + int get _recommendedHours { + final supply = _activeSupply; + if (supply == null) return 6; // Default + + final type = supply.type; + + if (type == PadType.menstrualCup || + type == PadType.menstrualDisc || + type == PadType.periodUnderwear) { + return 12; + } + + int baseHours; + switch (_selectedFlow) { + case FlowIntensity.heavy: + baseHours = (type == PadType.super_pad || + type == PadType.overnight || + type == PadType.tampon_super) + ? 4 + : 3; + break; + case FlowIntensity.medium: + baseHours = 6; + break; + case FlowIntensity.light: + baseHours = 8; + break; + case FlowIntensity.spotting: + baseHours = 10; // More generous for spotting + break; + case FlowIntensity.none: + baseHours = 8; // Health guideline for precautionary wear + break; + } + + int flowValue = 1; + switch (_selectedFlow) { + case FlowIntensity.none: + flowValue = 0; + break; // Health-only, no absorbency needed + case FlowIntensity.spotting: + flowValue = 1; + break; + case FlowIntensity.light: + flowValue = 2; + break; + case FlowIntensity.medium: + flowValue = 3; + break; + case FlowIntensity.heavy: + flowValue = 5; + break; } final absorbency = supply.absorbency; + + // Avoid division by zero for precautionary (no flow) case + if (flowValue == 0) { + return baseHours; + } + final ratio = absorbency / flowValue; double adjusted = baseHours * ratio; - int maxHours = (type == PadType.tampon_regular || type == PadType.tampon_super) - ? 8 - : 12; + int maxHours = + (type == PadType.tampon_regular || type == PadType.tampon_super) + ? 8 + : 12; if (adjusted < 1) adjusted = 1; if (adjusted > maxHours) adjusted = maxHours.toDouble(); @@ -305,271 +340,307 @@ class _PadTrackerScreenState extends ConsumerState { final user = ref.watch(userProfileProvider); return ProtectedContentWrapper( - title: 'Pad Tracker', - isProtected: user?.isSuppliesProtected ?? false, - userProfile: user, - child: Scaffold( - appBar: AppBar( - title: const Text('Pad Tracker'), - centerTitle: true, - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Supply Selection at the top as requested - _buildSectionHeader('Current Protection'), - const SizedBox(height: 12), - GestureDetector( - onTap: _showSupplyPicker, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).cardTheme.color, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), + title: 'Pad Tracker', + isProtected: user?.isSuppliesProtected ?? false, + userProfile: user, + child: Scaffold( + appBar: AppBar( + title: const Text('Pad Tracker'), + centerTitle: true, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Supply Selection at the top as requested + _buildSectionHeader('Current Protection'), + const SizedBox(height: 12), + GestureDetector( + onTap: _showSupplyPicker, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).cardTheme.color, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColors.menstrualPhase.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], ), - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.menstrualPhase.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon(Icons.inventory_2_outlined, color: AppColors.menstrualPhase), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - supply != null ? '${supply.brand} ${supply.type.label}' : 'No Supply Selected', - style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.menstrualPhase.withOpacity(0.1), + shape: BoxShape.circle, ), - if (supply != null) - Text( - 'Absorbency: ${supply.absorbency}/5 • Stock: ${supply.count}', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - ) - else - Text( - 'Tap to manage your supplies', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - ), - ], - ), + child: const Icon(Icons.inventory_2_outlined, + color: AppColors.menstrualPhase), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + supply != null + ? '${supply.brand} ${supply.type.label}' + : 'No Supply Selected', + style: GoogleFonts.outfit( + fontWeight: FontWeight.bold, fontSize: 16), + ), + if (supply != null) + Text( + 'Absorbency: ${supply.absorbency}/5 • Stock: ${supply.count}', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray), + ) + else + Text( + 'Tap to manage your supplies', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray), + ), + ], + ), + ), + const Icon(Icons.edit_outlined, + size: 20, color: AppColors.warmGray), + ], ), - const Icon(Icons.edit_outlined, size: 20, color: AppColors.warmGray), - ], - ), - ), - ), - - const SizedBox(height: 32), - - _buildSectionHeader('Current Flow Intensity'), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: FlowIntensity.values.map((flow) { - return ChoiceChip( - label: Text(flow.label), - selected: _selectedFlow == flow, - onSelected: (selected) { - if (selected) setState(() => _selectedFlow = flow); - }, - selectedColor: AppColors.menstrualPhase.withOpacity(0.3), - labelStyle: GoogleFonts.outfit( - color: _selectedFlow == flow ? AppColors.navyBlue : AppColors.charcoal, - fontWeight: _selectedFlow == flow ? FontWeight.w600 : FontWeight.w400, - ), - ); - }).toList(), - ), - const SizedBox(height: 48), - - // Recommendation Card / Timer - Center( - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: isOverdue ? AppColors.rose.withOpacity(0.15) : AppColors.sageGreen.withOpacity(0.15), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: isOverdue ? AppColors.rose.withOpacity(0.3) : AppColors.sageGreen.withOpacity(0.3) ), ), - child: Column( - children: [ - Icon( - isOverdue ? Icons.warning_amber_rounded : Icons.timer_outlined, - size: 48, - color: isOverdue ? AppColors.rose : AppColors.sageGreen - ), - const SizedBox(height: 16), - Text( - isOverdue ? 'Change Overdue!' : 'Next Change In:', - style: GoogleFonts.outfit( - fontSize: 16, - color: AppColors.warmGray, - ), - ), - const SizedBox(height: 8), - if (_timeSinceLastChange != Duration.zero) ...[ - Text( - _formatRemainingTime( - Duration(hours: _recommendedHours) - _timeSinceLastChange, - user! - ), - style: GoogleFonts.outfit( - fontSize: 32, - fontWeight: FontWeight.bold, - color: isOverdue ? AppColors.rose : AppColors.navyBlue, - ), - textAlign: TextAlign.center, - ), - Text( - 'Last changed: ${_formatDuration(_timeSinceLastChange, user)} ago', - style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray), - ), - ] else ...[ - Text( - '~$_recommendedHours Hours', - style: GoogleFonts.outfit( - fontSize: 32, - fontWeight: FontWeight.bold, - color: AppColors.navyBlue, - ), - ), - ], - ], - ), - ), - ), - const SizedBox(height: 32), - - if (_shouldShowMismatchWarning) - Container( - margin: const EdgeInsets.only(bottom: 24), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.rose.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.rose.withOpacity(0.3)), - ), - child: Row( - children: [ - const Icon(Icons.warning_amber_rounded, color: AppColors.rose), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Your flow is heavier than your protection capacity. Change sooner to avoid leaks!', - style: GoogleFonts.outfit( - fontSize: 14, - color: AppColors.charcoal, - fontWeight: FontWeight.w500 - ), - ), - ), - ], - ), - ), - - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton.icon( - onPressed: supply == null ? null : () async { - final hours = _recommendedHours; - - // 1. Auto-deduct inventory - if (user != null && - user.isAutoInventoryEnabled) { - - // Deduct from the active supply - final List updatedSupplies = user.padSupplies!.map((s) { - if (s == supply && s.count > 0) { - return s.copyWith(count: s.count - 1); - } - return s; - }).toList(); + const SizedBox(height: 32), - final updatedProfile = user.copyWith( - padSupplies: updatedSupplies, - lastInventoryUpdate: DateTime.now(), - lastPadChangeTime: DateTime.now(), - ); - await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); - } else if (user != null) { - final updatedProfile = user.copyWith( - lastPadChangeTime: DateTime.now(), - ); - await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); - } - - await NotificationService().scheduleNotification( - id: 100, - title: 'Time to change!', - body: 'It\'s been $hours hours since you logged your protection.', - scheduledDate: DateTime.now().add(Duration(hours: hours)), - ); - - setState(() { - _notificationScheduled = true; - _updateTimeSinceChange(); - }); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Logged! Timer reset & Inventory updated.')), - ); - } + _buildSectionHeader('Current Flow Intensity'), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: FlowIntensity.values.map((flow) { + return ChoiceChip( + label: Text(flow.label), + selected: _selectedFlow == flow, + onSelected: (selected) { + if (selected) setState(() => _selectedFlow = flow); }, - icon: Icon(_notificationScheduled ? Icons.check : Icons.restart_alt), - label: Text( - 'Changed / Remind Me', - style: GoogleFonts.outfit(fontSize: 18, fontWeight: FontWeight.w600), + selectedColor: AppColors.menstrualPhase.withOpacity(0.3), + labelStyle: GoogleFonts.outfit( + color: _selectedFlow == flow + ? AppColors.navyBlue + : AppColors.charcoal, + fontWeight: _selectedFlow == flow + ? FontWeight.w600 + : FontWeight.w400, + ), + ); + }).toList(), ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.menstrualPhase, - foregroundColor: Colors.white, - disabledBackgroundColor: AppColors.warmGray.withOpacity(0.2), + const SizedBox(height: 48), + + // Recommendation Card / Timer + Center( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: isOverdue + ? AppColors.rose.withOpacity(0.15) + : AppColors.sageGreen.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isOverdue + ? AppColors.rose.withOpacity(0.3) + : AppColors.sageGreen.withOpacity(0.3)), + ), + child: Column( + children: [ + Icon( + isOverdue + ? Icons.warning_amber_rounded + : Icons.timer_outlined, + size: 48, + color: isOverdue + ? AppColors.rose + : AppColors.sageGreen), + const SizedBox(height: 16), + Text( + isOverdue ? 'Change Overdue!' : 'Next Change In:', + style: GoogleFonts.outfit( + fontSize: 16, + color: AppColors.warmGray, + ), + ), + const SizedBox(height: 8), + if (_timeSinceLastChange != Duration.zero) ...[ + Text( + _formatRemainingTime( + Duration(hours: _recommendedHours) - + _timeSinceLastChange, + user!), + style: GoogleFonts.outfit( + fontSize: 32, + fontWeight: FontWeight.bold, + color: isOverdue + ? AppColors.rose + : AppColors.navyBlue, + ), + textAlign: TextAlign.center, + ), + Text( + 'Last changed: ${_formatDuration(_timeSinceLastChange, user)} ago', + style: GoogleFonts.outfit( + fontSize: 12, color: AppColors.warmGray), + ), + ] else ...[ + Text( + '~$_recommendedHours Hours', + style: GoogleFonts.outfit( + fontSize: 32, + fontWeight: FontWeight.bold, + color: AppColors.navyBlue, + ), + ), + ], + ], + ), + ), ), - ), + const SizedBox(height: 32), + + if (_shouldShowMismatchWarning) + Container( + margin: const EdgeInsets.only(bottom: 24), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.rose.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: AppColors.rose.withOpacity(0.3)), + ), + child: Row( + children: [ + const Icon(Icons.warning_amber_rounded, + color: AppColors.rose), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Your flow is heavier than your protection capacity. Change sooner to avoid leaks!', + style: GoogleFonts.outfit( + fontSize: 14, + color: AppColors.charcoal, + fontWeight: FontWeight.w500), + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: supply == null + ? null + : () async { + final hours = _recommendedHours; + + // 1. Auto-deduct inventory + if (user != null && user.isAutoInventoryEnabled) { + // Deduct from the active supply + final List updatedSupplies = + user.padSupplies!.map((s) { + if (s == supply && s.count > 0) { + return s.copyWith(count: s.count - 1); + } + return s; + }).toList(); + + final updatedProfile = user.copyWith( + padSupplies: updatedSupplies, + lastInventoryUpdate: DateTime.now(), + lastPadChangeTime: DateTime.now(), + ); + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); + } else if (user != null) { + final updatedProfile = user.copyWith( + lastPadChangeTime: DateTime.now(), + ); + await ref + .read(userProfileProvider.notifier) + .updateProfile(updatedProfile); + } + + await NotificationService().scheduleNotification( + id: 100, + title: 'Time to change!', + body: + 'It\'s been $hours hours since you logged your protection.', + scheduledDate: + DateTime.now().add(Duration(hours: hours)), + ); + + setState(() { + _notificationScheduled = true; + _updateTimeSinceChange(); + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Logged! Timer reset & Inventory updated.')), + ); + } + }, + icon: Icon(_notificationScheduled + ? Icons.check + : Icons.restart_alt), + label: Text( + 'Changed / Remind Me', + style: GoogleFonts.outfit( + fontSize: 18, fontWeight: FontWeight.w600), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.menstrualPhase, + foregroundColor: Colors.white, + disabledBackgroundColor: + AppColors.warmGray.withOpacity(0.2), + ), + ), + ), + ], ), - ], - ), - ), - )); + ), + )); } String _formatDuration(Duration d, UserProfile user) { final hours = d.inHours; final minutes = d.inMinutes % 60; final seconds = d.inSeconds % 60; - + final bool showMins = user.showPadTimerMinutes; - final bool showSecs = user.showPadTimerSeconds && showMins; // Enforce minutes must be shown to show seconds - + final bool showSecs = user.showPadTimerSeconds && + showMins; // Enforce minutes must be shown to show seconds + List parts = []; if (hours > 0) parts.add('${hours}h'); if (showMins) parts.add('${minutes}m'); if (showSecs) parts.add('${seconds}s'); - + if (parts.isEmpty) { if (hours == 0 && minutes == 0 && seconds == 0) return 'Just now'; return '${d.inMinutes}m'; // Fallback @@ -580,14 +651,15 @@ class _PadTrackerScreenState extends ConsumerState { String _formatRemainingTime(Duration remaining, UserProfile user) { final isOverdue = remaining.isNegative; final absRemaining = remaining.abs(); - + final hours = absRemaining.inHours; final minutes = absRemaining.inMinutes % 60; final seconds = absRemaining.inSeconds % 60; - + final bool showMins = user.showPadTimerMinutes; - final bool showSecs = user.showPadTimerSeconds && showMins; // Enforce minutes must be shown to show seconds - + final bool showSecs = user.showPadTimerSeconds && + showMins; // Enforce minutes must be shown to show seconds + List parts = []; if (hours > 0) parts.add('${hours}h'); if (showMins) { @@ -596,11 +668,11 @@ class _PadTrackerScreenState extends ConsumerState { if (showSecs) { parts.add('${seconds}s'); } - + if (parts.isEmpty) { return isOverdue ? 'Overdue' : 'Change Now'; } - + String timeStr = parts.join(' '); return isOverdue ? '$timeStr overdue' : timeStr; } @@ -621,10 +693,12 @@ class _SupplyManagementPopup extends ConsumerStatefulWidget { const _SupplyManagementPopup(); @override - ConsumerState<_SupplyManagementPopup> createState() => _SupplyManagementPopupState(); + ConsumerState<_SupplyManagementPopup> createState() => + _SupplyManagementPopupState(); } -class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> { +class _SupplyManagementPopupState + extends ConsumerState<_SupplyManagementPopup> { final _brandController = TextEditingController(); PadType _selectedType = PadType.regular; int _absorbency = 3; @@ -650,9 +724,12 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> count: _count, ); - final List updatedSupplies = [...(user.padSupplies ?? []), newSupply]; + final List updatedSupplies = [ + ...(user.padSupplies ?? []), + newSupply + ]; final updatedProfile = user.copyWith(padSupplies: updatedSupplies); - + await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); _brandController.clear(); setState(() { @@ -686,7 +763,8 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> children: [ Text( 'Manage Supplies', - style: GoogleFonts.outfit(fontSize: 20, fontWeight: FontWeight.bold), + style: GoogleFonts.outfit( + fontSize: 20, fontWeight: FontWeight.bold), ), IconButton( icon: const Icon(Icons.close), @@ -696,9 +774,10 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> ), const Divider(), const SizedBox(height: 16), - if (supplies.isNotEmpty) ...[ - Text('Current Stock', style: GoogleFonts.outfit(fontWeight: FontWeight.w600, color: AppColors.navyBlue)), + Text('Current Stock', + style: GoogleFonts.outfit( + fontWeight: FontWeight.w600, color: AppColors.navyBlue)), const SizedBox(height: 12), SizedBox( height: 120, @@ -714,7 +793,8 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> decoration: BoxDecoration( color: AppColors.warmCream.withOpacity(0.3), borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.warmGray.withOpacity(0.2)), + border: Border.all( + color: AppColors.warmGray.withOpacity(0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -723,24 +803,41 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: Text(item.brand, style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 13), overflow: TextOverflow.ellipsis), + child: Text(item.brand, + style: GoogleFonts.outfit( + fontWeight: FontWeight.bold, + fontSize: 13), + overflow: TextOverflow.ellipsis), ), GestureDetector( - onTap: () async { - final updatedSupplies = List.from(supplies)..removeAt(index); - await ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(padSupplies: updatedSupplies)); - }, - child: const Icon(Icons.delete_outline, size: 16, color: Colors.red), + onTap: () async { + final updatedSupplies = + List.from(supplies) + ..removeAt(index); + await ref + .read(userProfileProvider.notifier) + .updateProfile(user!.copyWith( + padSupplies: updatedSupplies)); + }, + child: const Icon(Icons.delete_outline, + size: 16, color: Colors.red), ), ], ), - Text(item.type.label, style: GoogleFonts.outfit(fontSize: 11, color: AppColors.warmGray)), + Text(item.type.label, + style: GoogleFonts.outfit( + fontSize: 11, color: AppColors.warmGray)), const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Qty: ${item.count}', style: GoogleFonts.outfit(fontSize: 12, fontWeight: FontWeight.w600)), - Text('Abs: ${item.absorbency}', style: GoogleFonts.outfit(fontSize: 11, color: AppColors.menstrualPhase)), + Text('Qty: ${item.count}', + style: GoogleFonts.outfit( + fontSize: 12, fontWeight: FontWeight.w600)), + Text('Abs: ${item.absorbency}', + style: GoogleFonts.outfit( + fontSize: 11, + color: AppColors.menstrualPhase)), ], ), ], @@ -751,8 +848,9 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> ), const SizedBox(height: 24), ], - - Text('Add New Pack', style: GoogleFonts.outfit(fontWeight: FontWeight.w600, color: AppColors.navyBlue)), + Text('Add New Pack', + style: GoogleFonts.outfit( + fontWeight: FontWeight.w600, color: AppColors.navyBlue)), const SizedBox(height: 12), TextField( controller: _brandController, @@ -760,8 +858,11 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> hintText: 'Brand Name (e.g. Always)', filled: true, fillColor: AppColors.warmCream.withOpacity(0.2), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ), const SizedBox(height: 12), @@ -772,9 +873,15 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> value: _selectedType, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12)), ), - items: PadType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.label, style: const TextStyle(fontSize: 13)))).toList(), + items: PadType.values + .map((t) => DropdownMenuItem( + value: t, + child: Text(t.label, + style: const TextStyle(fontSize: 13)))) + .toList(), onChanged: (val) => setState(() => _selectedType = val!), ), ), @@ -788,19 +895,29 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> ), child: Row( children: [ - IconButton(icon: const Icon(Icons.remove, size: 16), onPressed: () => setState(() => _count = (_count > 0 ? _count - 1 : 0))), - Text('$_count', style: const TextStyle(fontWeight: FontWeight.bold)), - IconButton(icon: const Icon(Icons.add, size: 16), onPressed: () => setState(() => _count++)), + IconButton( + icon: const Icon(Icons.remove, size: 16), + onPressed: () => setState( + () => _count = (_count > 0 ? _count - 1 : 0))), + Text('$_count', + style: const TextStyle(fontWeight: FontWeight.bold)), + IconButton( + icon: const Icon(Icons.add, size: 16), + onPressed: () => setState(() => _count++)), ], ), ), ], ), const SizedBox(height: 16), - Text('Absorbency: $_absorbency/5', style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)), + Text('Absorbency: $_absorbency/5', + style: + GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)), Slider( value: _absorbency.toDouble(), - min: 1, max: 5, divisions: 4, + min: 1, + max: 5, + divisions: 4, activeColor: AppColors.menstrualPhase, onChanged: (val) => setState(() => _absorbency = val.round()), ), @@ -810,7 +927,9 @@ class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> height: 50, child: ElevatedButton( onPressed: _addSupply, - style: ElevatedButton.styleFrom(backgroundColor: AppColors.navyBlue, foregroundColor: Colors.white), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.navyBlue, + foregroundColor: Colors.white), child: const Text('Add to Inventory'), ), ), diff --git a/lib/screens/onboarding/onboarding_screen.dart b/lib/screens/onboarding/onboarding_screen.dart index 70b784b..6b69ba6 100644 --- a/lib/screens/onboarding/onboarding_screen.dart +++ b/lib/screens/onboarding/onboarding_screen.dart @@ -4,12 +4,13 @@ import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:uuid/uuid.dart'; import '../../theme/app_theme.dart'; -import 'package:christian_period_tracker/models/user_profile.dart'; -import 'package:christian_period_tracker/models/cycle_entry.dart'; +import '../../models/user_profile.dart'; +import '../../models/cycle_entry.dart'; import '../home/home_screen.dart'; import '../husband/husband_home_screen.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/user_provider.dart'; +import '../../services/notification_service.dart'; class OnboardingScreen extends ConsumerStatefulWidget { const OnboardingScreen({super.key}); @@ -35,6 +36,10 @@ class _OnboardingScreenState extends ConsumerState { int _maxCycleLength = 35; bool _isPadTrackingEnabled = false; + // Connection options + bool _useExampleData = false; + bool _skipPartnerConnection = false; + @override void dispose() { _pageController.dispose(); @@ -45,14 +50,15 @@ class _OnboardingScreenState extends ConsumerState { if (_isNavigating) return; _isNavigating = true; - // Husband Flow: Role (0) -> Name (1) -> Finish - // Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4) + // Husband Flow: Role (0) -> Name (1) -> Connect (2) -> Finish + // Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4) -> [Connect (5) if married] int nextPage = _currentPage + 1; // Logic for skipping pages if (_role == UserRole.husband) { - if (_currentPage == 1) { + if (_currentPage == 2) { + // Finish after connect page await _completeOnboarding(); return; } @@ -63,10 +69,21 @@ class _OnboardingScreenState extends ConsumerState { // Skip fertility goal (page 3) if not married nextPage = 4; } + if (_currentPage == 4 && + _relationshipStatus != RelationshipStatus.married) { + // Skip connect page (page 5) if not married - finish now + await _completeOnboarding(); + return; + } + if (_currentPage == 5) { + // Finish after connect page (married wife) + await _completeOnboarding(); + return; + } } - if (nextPage <= 4) { - // Max pages + final maxPages = _role == UserRole.husband ? 2 : 5; + if (nextPage <= maxPages) { await _pageController.animateToPage( nextPage, duration: const Duration(milliseconds: 400), @@ -124,18 +141,24 @@ class _OnboardingScreenState extends ConsumerState { ? _fertilityGoal : null, averageCycleLength: _averageCycleLength, - minCycleLength: _minCycleLength, - maxCycleLength: _maxCycleLength, lastPeriodStartDate: _lastPeriodStart, isIrregularCycle: _isIrregularCycle, - isPadTrackingEnabled: _isPadTrackingEnabled, hasCompletedOnboarding: true, + useExampleData: _useExampleData, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); await ref.read(userProfileProvider.notifier).updateProfile(userProfile); + // Trigger partner connection notification if applicable + if (!_skipPartnerConnection && !_useExampleData) { + await NotificationService().showPartnerUpdateNotification( + title: 'Connection Successful!', + body: 'You are now connected with your partner. Tap to start sharing.', + ); + } + if (mounted) { // Navigate to appropriate home screen if (_role == UserRole.husband) { @@ -174,7 +197,11 @@ class _OnboardingScreenState extends ConsumerState { padding: const EdgeInsets.all(24), child: SmoothPageIndicator( controller: _pageController, - count: isHusband ? 2 : 5, + count: isHusband + ? 3 + : (_relationshipStatus == RelationshipStatus.married + ? 6 + : 5), effect: WormEffect( dotHeight: 8, dotWidth: 8, @@ -190,17 +217,22 @@ class _OnboardingScreenState extends ConsumerState { Expanded( child: PageView( controller: _pageController, - physics: - const NeverScrollableScrollPhysics(), // Disable swipe + physics: const NeverScrollableScrollPhysics(), // Disable swipe onPageChanged: (index) { setState(() => _currentPage = index); }, children: [ _buildRolePage(), // Page 0 _buildNamePage(), // Page 1 - _buildRelationshipPage(), // Page 2 (Wife only) - _buildFertilityGoalPage(), // Page 3 (Wife married only) - _buildCyclePage(), // Page 4 (Wife only) + if (_role == UserRole.husband) + _buildHusbandConnectPage() // Page 2 (Husband only) + else ...[ + _buildRelationshipPage(), // Page 2 (Wife only) + _buildFertilityGoalPage(), // Page 3 (Wife married only) + _buildCyclePage(), // Page 4 (Wife only) + if (_relationshipStatus == RelationshipStatus.married) + _buildWifeConnectPage(), // Page 5 (Wife married only) + ], ], ), ), @@ -223,10 +255,7 @@ class _OnboardingScreenState extends ConsumerState { height: 80, decoration: BoxDecoration( gradient: LinearGradient( - colors: [ - AppColors.blushPink, - AppColors.rose.withOpacity(0.7) - ], + colors: [AppColors.blushPink, AppColors.rose.withOpacity(0.7)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), @@ -304,9 +333,8 @@ class _OnboardingScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: isSelected - ? activeColor - : theme.colorScheme.surfaceVariant, + color: + isSelected ? activeColor : theme.colorScheme.surfaceVariant, shape: BoxShape.circle, ), child: Icon( @@ -340,8 +368,7 @@ class _OnboardingScreenState extends ConsumerState { ], ), ), - if (isSelected) - Icon(Icons.check_circle, color: activeColor), + if (isSelected) Icon(Icons.check_circle, color: activeColor), ], ), ), @@ -351,8 +378,7 @@ class _OnboardingScreenState extends ConsumerState { Widget _buildNamePage() { final theme = Theme.of(context); final isHusband = _role == UserRole.husband; - final activeColor = - isHusband ? AppColors.navyBlue : AppColors.sageGreen; + final activeColor = isHusband ? AppColors.navyBlue : AppColors.sageGreen; return Padding( padding: const EdgeInsets.all(32), @@ -413,9 +439,8 @@ class _OnboardingScreenState extends ConsumerState { child: SizedBox( height: 54, child: ElevatedButton( - onPressed: (_name.isNotEmpty && !_isNavigating) - ? _nextPage - : null, + onPressed: + (_name.isNotEmpty && !_isNavigating) ? _nextPage : null, style: ElevatedButton.styleFrom( backgroundColor: activeColor, ), @@ -557,11 +582,8 @@ class _OnboardingScreenState extends ConsumerState { fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface)), const SizedBox(height: 32), - _buildGoalOption( - FertilityGoal.tryingToConceive, - 'Trying to Conceive', - 'Track fertile days', - Icons.child_care_outlined), + _buildGoalOption(FertilityGoal.tryingToConceive, 'Trying to Conceive', + 'Track fertile days', Icons.child_care_outlined), const SizedBox(height: 12), _buildGoalOption( FertilityGoal.tryingToAvoid, @@ -692,8 +714,7 @@ class _OnboardingScreenState extends ConsumerState { ), Text('$_averageCycleLength days', style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: AppColors.sageGreen)), + fontWeight: FontWeight.w600, color: AppColors.sageGreen)), ], ), @@ -720,12 +741,14 @@ class _OnboardingScreenState extends ConsumerState { children: [ Expanded( child: RangeSlider( - values: RangeValues(_minCycleLength.toDouble(), _maxCycleLength.toDouble()), + values: RangeValues( + _minCycleLength.toDouble(), _maxCycleLength.toDouble()), min: 21, max: 45, divisions: 24, activeColor: AppColors.sageGreen, - labels: RangeLabels('$_minCycleLength days', '$_maxCycleLength days'), + labels: RangeLabels( + '$_minCycleLength days', '$_maxCycleLength days'), onChanged: (values) { setState(() { _minCycleLength = values.start.round(); @@ -739,8 +762,7 @@ class _OnboardingScreenState extends ConsumerState { Center( child: Text('$_minCycleLength - $_maxCycleLength days', style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: AppColors.sageGreen)), + fontWeight: FontWeight.w600, color: AppColors.sageGreen)), ), ], @@ -840,4 +862,267 @@ class _OnboardingScreenState extends ConsumerState { ), ); } + + /// 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), + ], + ), + ), + ); + } } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 96e3c6f..21e873b 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -34,12 +34,13 @@ class NotificationService { requestBadgePermission: true, requestSoundPermission: true, ); - + // Linux initialization (optional, but good for completeness) final LinuxInitializationSettings initializationSettingsLinux = LinuxInitializationSettings(defaultActionName: 'Open notification'); - final InitializationSettings initializationSettings = InitializationSettings( + final InitializationSettings initializationSettings = + InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsDarwin, macOS: initializationSettingsDarwin, @@ -63,10 +64,10 @@ class NotificationService { required DateTime scheduledDate, }) async { if (kIsWeb) { - // Web platform limitation: Background scheduling is complex. - // For this demo/web preview, we'll just log it or rely on the UI confirmation. - print('Web Notification Scheduled: $title - $body at $scheduledDate'); - return; + // Web platform limitation: Background scheduling is complex. + // For this demo/web preview, we'll just log it or rely on the UI confirmation. + print('Web Notification Scheduled: $title - $body at $scheduledDate'); + return; } await flutterLocalNotificationsPlugin.zonedSchedule( @@ -92,30 +93,72 @@ class NotificationService { // New method for specific notification types Future showLocalNotification({ - required int id, - required String title, - required String body, - String? channelId, - String? channelName, + required int id, + required String title, + required String body, + String? channelId, + String? channelName, }) async { if (kIsWeb) { print('Web Local Notification: $title - $body'); return; } const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'tracker_general', 'General Notifications', + AndroidNotificationDetails('tracker_general', 'General Notifications', channelDescription: 'General app notifications', importance: Importance.max, priority: Priority.high, ticker: 'ticker'); - + const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails); - - await flutterLocalNotificationsPlugin.show( - id, title, body, notificationDetails, - payload: 'item x'); + + await flutterLocalNotificationsPlugin + .show(id, title, body, notificationDetails, payload: 'item x'); + } + + Future 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 showTeachingPlanNotification( + {required String teacherName}) async { + await showLocalNotification( + id: 302, + title: 'New Teaching Plan', + body: '$teacherName added a new teaching plan for you.', + ); + } + + Future showPartnerUpdateNotification( + {required String title, required String body}) async { + await showLocalNotification( + id: 305, + title: title, + body: body, + ); + } + + Future showCycleUpdateNotification({required String message}) async { + await showLocalNotification( + id: 310, + title: 'Cycle Update', + body: message, + ); + } + + Future 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 cancelNotification(int id) async { diff --git a/lib/widgets/pad_tracker_card.dart b/lib/widgets/pad_tracker_card.dart index 748e7b1..39e2e04 100644 --- a/lib/widgets/pad_tracker_card.dart +++ b/lib/widgets/pad_tracker_card.dart @@ -17,6 +17,9 @@ class PadTrackerCard extends ConsumerStatefulWidget { class _PadTrackerCardState extends ConsumerState { Timer? _timer; String _timeDisplay = ''; + double _progress = 0.0; + Color _statusColor = AppColors.menstrualPhase; + bool _isCountDown = true; // Toggle state @override void initState() { @@ -40,35 +43,92 @@ class _PadTrackerCardState extends ConsumerState { void _updateTime() { final user = ref.read(userProfileProvider); if (user?.lastPadChangeTime == null) { - if (mounted) setState(() => _timeDisplay = 'Tap to start'); + if (mounted) { + setState(() { + _timeDisplay = 'Tap to start'; + _progress = 0; + _statusColor = AppColors.menstrualPhase; + }); + } return; } final now = DateTime.now(); final difference = now.difference(user!.lastPadChangeTime!); - - // We want to show time SINCE change (duration worn) - final hours = difference.inHours; - final minutes = difference.inMinutes.remainder(60); - final seconds = difference.inSeconds.remainder(60); + + // Estimate max duration based on flow + // None/Precautionary: 8h, Spotting: 8h, Light: 6h, Medium: 4h, Heavy: 3h + final flowIntensity = user.typicalFlowIntensity ?? 2; // Default to light + Duration maxDuration; + switch (flowIntensity) { + case 0: // No Flow / Precautionary (health guideline: 8h max) + maxDuration = const Duration(hours: 8); + break; + case 1: // Spotting + maxDuration = const Duration(hours: 8); + break; + case 2: // Light + maxDuration = const Duration(hours: 6); + break; + case 3: // Medium + maxDuration = const Duration(hours: 4); + break; + case 4: // Heavy + maxDuration = const Duration(hours: 3); + break; + case 5: // Very Heavy + maxDuration = const Duration(hours: 2); + break; + default: + maxDuration = const Duration(hours: 4); + } + + final totalSeconds = maxDuration.inSeconds; + final elapsedSeconds = difference.inSeconds; + double progress = elapsedSeconds / totalSeconds; + progress = progress.clamp(0.0, 1.0); + + // Determine Status Color + Color newColor = AppColors.menstrualPhase; + if (progress > 0.9) { + newColor = Colors.red; + } else if (progress > 0.75) { + newColor = Colors.orange; + } else { + newColor = AppColors.menstrualPhase; // Greenish/Theme color + } + // Override if we want to visually show "fresh" vs "old" String text = ''; - - if (user.showPadTimerMinutes) { - if (hours > 0) text += '$hours hr '; - text += '$minutes min'; - } - - if (user.showPadTimerSeconds) { - if (text.isNotEmpty) text += ' '; - text += '$seconds sec'; - } - if (text.isEmpty) text = 'Active'; // Fallback + if (_isCountDown) { + final remaining = maxDuration - difference; + final isOverdue = remaining.isNegative; + final absRemaining = remaining.abs(); + final hours = absRemaining.inHours; + final mins = absRemaining.inMinutes % 60; + + if (isOverdue) { + text = 'Overdue by ${hours}h ${mins}m'; + newColor = Colors.red; // Force red if overdue + } else { + text = '${hours}h ${mins}m left'; + } + _progress = + isOverdue ? 1.0 : (1.0 - progress); // Depleting if not overdue + } else { + // Count Up + final hours = difference.inHours; + final minutes = difference.inMinutes % 60; + text = '${hours}h ${minutes}m worn'; + _progress = progress; // Filling + } if (mounted) { setState(() { _timeDisplay = text; + _progress = _isCountDown && text.contains('Overdue') ? 1.0 : _progress; + _statusColor = newColor; }); } } @@ -76,10 +136,8 @@ class _PadTrackerCardState extends ConsumerState { @override Widget build(BuildContext context) { final user = ref.watch(userProfileProvider); - if (user == null || !user.isPadTrackingEnabled) return const SizedBox.shrink(); - - // Re-check time on rebuilds in case settings changed - // _updateTime(); // Actually let the timer handle it, or use a key to rebuild on setting changes + if (user == null || !user.isPadTrackingEnabled) + return const SizedBox.shrink(); return GestureDetector( onTap: () { @@ -91,53 +149,86 @@ class _PadTrackerCardState extends ConsumerState { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.menstrualPhase.withOpacity(0.1), + color: Colors.white, borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)), + border: Border.all(color: _statusColor.withOpacity(0.3)), boxShadow: [ - BoxShadow( - color: AppColors.menstrualPhase.withOpacity(0.05), - blurRadius: 8, + BoxShadow( + color: _statusColor.withOpacity(0.1), + blurRadius: 10, offset: const Offset(0, 4), ), ], ), - child: Row( + child: Column( children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.menstrualPhase.withOpacity(0.2), - shape: BoxShape.circle, - ), - child: const Icon(Icons.timer_outlined, color: AppColors.menstrualPhase, size: 24), + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: _statusColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: + Icon(Icons.timer_outlined, color: _statusColor, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Pad Tracker', + style: GoogleFonts.outfit( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.charcoal, + ), + ), + InkWell( + onTap: () { + setState(() { + _isCountDown = !_isCountDown; + _updateTime(); + }); + }, + child: Icon( + _isCountDown + ? Icons.arrow_downward + : Icons.arrow_upward, + size: 16, + color: AppColors.warmGray), + ) + ], + ), + const SizedBox(height: 4), + Text( + _timeDisplay.isNotEmpty ? _timeDisplay : 'Tap to track', + style: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.w500, + color: _statusColor), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: AppColors.lightGray), + ], ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Pad Tracker', - style: GoogleFonts.outfit( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.charcoal, - ), - ), - const SizedBox(height: 4), - Text( - _timeDisplay.isNotEmpty ? _timeDisplay : 'Tap to track', - style: GoogleFonts.outfit( - fontSize: 14, - fontWeight: FontWeight.w500, - color: AppColors.menstrualPhase - ), - ), - ], + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: _progress, + backgroundColor: _statusColor.withOpacity(0.1), + valueColor: AlwaysStoppedAnimation(_statusColor), + minHeight: 6, ), ), - const Icon(Icons.chevron_right, color: AppColors.menstrualPhase), ], ), ), diff --git a/lib/widgets/quick_log_buttons.dart b/lib/widgets/quick_log_buttons.dart index e8a773c..0a3b411 100644 --- a/lib/widgets/quick_log_buttons.dart +++ b/lib/widgets/quick_log_buttons.dart @@ -4,6 +4,7 @@ import 'package:google_fonts/google_fonts.dart'; import '../providers/user_provider.dart'; import '../theme/app_theme.dart'; import '../providers/navigation_provider.dart'; +import '../models/user_profile.dart'; import 'quick_log_dialog.dart'; class QuickLogButtons extends ConsumerWidget { @@ -63,6 +64,13 @@ class QuickLogButtons extends ConsumerWidget { color: AppColors.lutealPhase, onTap: () => _showQuickLogDialog(context, 'pads'), ), + _buildQuickButton( + context, + icon: Icons.church_outlined, + label: 'Prayer', + color: AppColors.softGold, // Or a suitable color + onTap: () => _showQuickLogDialog(context, 'prayer'), + ), ], ), ); @@ -96,8 +104,7 @@ class QuickLogButtons extends ConsumerWidget { decoration: BoxDecoration( color: color.withOpacity(isDark ? 0.2 : 0.15), borderRadius: BorderRadius.circular(12), - border: - isDark ? Border.all(color: color.withOpacity(0.3)) : null, + border: isDark ? Border.all(color: color.withOpacity(0.3)) : null, ), child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/quick_log_dialog.dart b/lib/widgets/quick_log_dialog.dart index 7022e78..22892c2 100644 --- a/lib/widgets/quick_log_dialog.dart +++ b/lib/widgets/quick_log_dialog.dart @@ -23,7 +23,7 @@ class _QuickLogDialogState extends ConsumerState { FlowIntensity? _flowIntensity; MoodLevel? _mood; int? _energyLevel; - + // Symptoms & Cravings final Map _symptoms = { 'Headache': false, @@ -37,7 +37,7 @@ class _QuickLogDialogState extends ConsumerState { 'Insomnia': false, 'Cramps': false, }; - + final TextEditingController _cravingController = TextEditingController(); List _cravings = []; List _recentCravings = []; @@ -102,6 +102,8 @@ class _QuickLogDialogState extends ConsumerState { return _buildSymptomsLog(); case 'cravings': return _buildCravingsLog(); + case 'prayer': + return _buildPrayerLog(); default: return const Text('Invalid log type.'); } @@ -137,7 +139,7 @@ class _QuickLogDialogState extends ConsumerState { } Widget _buildCravingsLog() { - return Container( + return Container( width: double.maxFinite, child: Column( mainAxisSize: MainAxisSize.min, @@ -152,37 +154,44 @@ class _QuickLogDialogState extends ConsumerState { ), onSubmitted: (value) { if (value.isNotEmpty) { - setState(() { - _cravings.add(value.trim()); - _cravingController.clear(); - }); + setState(() { + _cravings.add(value.trim()); + _cravingController.clear(); + }); } }, ), const SizedBox(height: 8), Wrap( spacing: 8, - children: _cravings.map((c) => Chip( - label: Text(c), - onDeleted: () { - setState(() => _cravings.remove(c)); - }, - )).toList(), + children: _cravings + .map((c) => Chip( + label: Text(c), + onDeleted: () { + setState(() => _cravings.remove(c)); + }, + )) + .toList(), ), const SizedBox(height: 16), if (_recentCravings.isNotEmpty) ...[ - Text('Recent Cravings:', style: GoogleFonts.outfit(fontSize: 12, fontWeight: FontWeight.bold)), + Text('Recent Cravings:', + style: GoogleFonts.outfit( + fontSize: 12, fontWeight: FontWeight.bold)), const SizedBox(height: 8), - Wrap( + Wrap( spacing: 8, - children: _recentCravings.take(5).map((c) => ActionChip( - label: Text(c), - onPressed: () { - if (!_cravings.contains(c)) { - setState(() => _cravings.add(c)); - } - }, - )).toList(), + children: _recentCravings + .take(5) + .map((c) => ActionChip( + label: Text(c), + onPressed: () { + if (!_cravings.contains(c)) { + setState(() => _cravings.add(c)); + } + }, + )) + .toList(), ), ] ], @@ -277,10 +286,30 @@ class _QuickLogDialogState extends ConsumerState { ); } + 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 _saveLog() async { // Handle text input for cravings if user didn't hit enter if (widget.logType == 'cravings' && _cravingController.text.isNotEmpty) { - _cravings.add(_cravingController.text.trim()); + _cravings.add(_cravingController.text.trim()); } final cycleNotifier = ref.read(cycleEntriesProvider.notifier); @@ -288,7 +317,11 @@ class _QuickLogDialogState extends ConsumerState { final entries = ref.read(cycleEntriesProvider); final entry = entries.firstWhere( (e) => DateUtils.isSameDay(e.date, today), - orElse: () => CycleEntry(id: const Uuid().v4(), date: today, createdAt: today, updatedAt: today), + orElse: () => CycleEntry( + id: const Uuid().v4(), + date: today, + createdAt: today, + updatedAt: today), ); CycleEntry updatedEntry = entry; @@ -308,28 +341,61 @@ class _QuickLogDialogState extends ConsumerState { break; case 'symptoms': updatedEntry = entry.copyWith( - hasHeadache: _symptoms['Headache'], - hasBloating: _symptoms['Bloating'], - hasBreastTenderness: _symptoms['Breast Tenderness'], - hasFatigue: _symptoms['Fatigue'], - hasAcne: _symptoms['Acne'], - hasLowerBackPain: _symptoms['Back Pain'], - hasConstipation: _symptoms['Constipation'], - hasDiarrhea: _symptoms['Diarrhea'], - hasInsomnia: _symptoms['Insomnia'], - crampIntensity: _symptoms['Cramps'] == true ? 2 : 0, // Default to mild cramps if just toggled + hasHeadache: _symptoms['Headache'], + hasBloating: _symptoms['Bloating'], + hasBreastTenderness: _symptoms['Breast Tenderness'], + hasFatigue: _symptoms['Fatigue'], + hasAcne: _symptoms['Acne'], + hasLowerBackPain: _symptoms['Back Pain'], + hasConstipation: _symptoms['Constipation'], + hasDiarrhea: _symptoms['Diarrhea'], + hasInsomnia: _symptoms['Insomnia'], + crampIntensity: _symptoms['Cramps'] == true + ? 2 + : 0, // Default to mild cramps if just toggled ); + // Trigger notification if any symptom is selected + final user = ref.read(userProfileProvider); + if (_symptoms.values.any((selected) => selected == true)) { + final selectedSymptom = _symptoms.entries + .firstWhere((element) => element.value == true) + .key; + NotificationService().showSymptomNotification( + senderName: user?.name ?? 'Wife', + symptom: selectedSymptom, + ); + } break; case 'cravings': final currentCravings = entry.cravings ?? []; final newCravings = {...currentCravings, ..._cravings}.toList(); updatedEntry = entry.copyWith(cravings: newCravings); - + // Update History final prefs = await SharedPreferences.getInstance(); final history = prefs.getStringList('recent_cravings') ?? []; final updatedHistory = {..._cravings, ...history}.take(20).toList(); await prefs.setStringList('recent_cravings', updatedHistory); + await prefs.setStringList('recent_cravings', updatedHistory); + break; + case 'prayer': + final currentPrayer = entry.prayerRequest ?? ''; + final newPrayer = + _cravingController.text.trim(); // Using reused controller + if (newPrayer.isNotEmpty) { + updatedEntry = entry.copyWith( + prayerRequest: currentPrayer.isEmpty + ? newPrayer + : '$currentPrayer\n$newPrayer'); + + // Trigger notification + final user = ref.read(userProfileProvider); + NotificationService().showPrayerRequestNotification( + senderName: user?.name ?? 'Wife', + ); + } else { + return; // Don't save empty prayer + } break; default: // pads handled separately