feat: Implement husband features and fix iOS Safari web startup

Implement initial features for husband's companion app, including mock data
service and husband notes screen. Refactor scripture and cycle services
for improved stability and testability. Address iOS Safari web app
startup issue by removing deprecated initialization.

- Implemented MockDataService and HusbandNotesScreen.
- Converted _DashboardTab and DevotionalScreen to StatefulWidgets for robust
  scripture provider initialization.
- Refactored CycleService to use immutable CycleInfo class, reducing UI rebuilds.
- Removed deprecated window.flutterConfiguration from index.html, resolving
  Flutter web app startup failure on iOS Safari.
- Updated and fixed related tests.
This commit is contained in:
2025-12-26 22:40:52 -06:00
parent 464692ce56
commit b4b2bfe749
47 changed files with 240110 additions and 2578 deletions

View File

@@ -1,3 +1,4 @@
import 'package:christian_period_tracker/models/scripture.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -7,13 +8,15 @@ import '../../models/cycle_entry.dart';
import '../../providers/user_provider.dart';
import '../../services/cycle_service.dart';
import '../../theme/app_theme.dart';
<<<<<<< HEAD
=======
import '../log/log_screen.dart';
>>>>>>> 6742220 (Your commit message here)
class CalendarScreen extends ConsumerStatefulWidget {
const CalendarScreen({super.key});
final bool readOnly;
const CalendarScreen({
super.key,
this.readOnly = false,
});
@override
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
@@ -33,7 +36,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
return SafeArea(
child: Column(
children: [
children: [
// Header
Padding(
padding: const EdgeInsets.all(20),
@@ -145,46 +148,12 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
),
),
calendarBuilders: CalendarBuilders(
<<<<<<< HEAD
markerBuilder: (context, date, events) {
// Check if it's a logged period day
final isLoggedPeriod = _isLoggedPeriodDay(date, entries);
if (isLoggedPeriod) {
return Positioned(
bottom: 1,
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: AppColors.menstrualPhase,
shape: BoxShape.circle,
),
),
);
}
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
if (phase != null) {
return Positioned(
bottom: 1,
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.5),
shape: BoxShape.circle,
),
),
);
}
return null;
=======
markerBuilder: (context, date, entries) {
final entry = _getEntryForDate(date, entries);
if (entry == null) {
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
final phase =
_getPhaseForDate(date, lastPeriodStart, cycleLength);
if (phase != null) {
return Positioned(
bottom: 1,
@@ -200,7 +169,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
}
return null;
}
// If we have an entry, show icons/markers
return Positioned(
bottom: 1,
@@ -217,7 +186,9 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
shape: BoxShape.circle,
),
),
if (entry.mood != null || entry.energyLevel != 3 || entry.hasSymptoms)
if (entry.mood != null ||
entry.energyLevel != 3 ||
entry.hasSymptoms)
Container(
width: 6,
height: 6,
@@ -230,7 +201,6 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
],
),
);
>>>>>>> 6742220 (Your commit message here)
},
),
),
@@ -241,7 +211,8 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
// Selected Day Info
if (_selectedDay != null)
Expanded(
child: _buildDayInfo(_selectedDay!, lastPeriodStart, cycleLength, entries),
child: _buildDayInfo(
_selectedDay!, lastPeriodStart, cycleLength, entries),
),
],
),
@@ -333,20 +304,11 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
);
}
Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, int cycleLength, List<CycleEntry> entries) {
Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, int cycleLength,
List<CycleEntry> entries) {
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
final entry = _getEntryForDate(date, entries);
<<<<<<< HEAD
final isLoggedPeriod = entry?.isPeriodDay ?? false;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
=======
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(20),
@@ -360,94 +322,31 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
offset: const Offset(0, 4),
),
],
>>>>>>> 6742220 (Your commit message here)
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
<<<<<<< HEAD
Text(
'${_getMonthName(date.month)} ${date.day}, ${date.year}',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 12),
if (phase != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(phase.emoji),
const SizedBox(width: 6),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
const SizedBox(height: 12),
if (isLoggedPeriod)
Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.menstrualPhase.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.water_drop, color: AppColors.menstrualPhase, size: 20),
const SizedBox(width: 8),
Text(
'Period Recorded',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.menstrualPhase,
),
),
],
),
),
Text(
phase?.description ?? 'No cycle data for this date',
style: GoogleFonts.outfit(
fontSize: 14,
=======
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_getMonthName(date.month)} ${date.day}, ${date.year}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
fontWeight: FontWeight.w600,
),
),
if (entry != null)
const Icon(Icons.check_circle, color: AppColors.sageGreen, size: 20),
const Icon(Icons.check_circle,
color: AppColors.sageGreen, size: 20),
],
),
const SizedBox(height: 16),
if (phase != null)
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
@@ -469,34 +368,38 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
),
),
),
if (entry == null)
Text(
phase?.description ?? 'No data for this date',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: AppColors.warmGray,
),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: AppColors.warmGray),
)
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
if (entry.mood != null)
_buildDetailRow(Icons.emoji_emotions_outlined, 'Mood', AppColors.softGold,
_buildDetailRow(
Icons.emoji_emotions_outlined, 'Mood', AppColors.softGold,
value: '${entry.mood!.emoji} ${entry.mood!.label}'),
// Energy Detail
_buildDetailRow(Icons.flash_on, 'Energy Level', AppColors.follicularPhase,
_buildDetailRow(
Icons.flash_on, 'Energy Level', AppColors.follicularPhase,
value: _getEnergyLabel(entry.energyLevel)),
// Symptoms
if (entry.hasSymptoms)
_buildDetailRow(Icons.healing_outlined, 'Symptoms', AppColors.lavender,
_buildDetailRow(
Icons.healing_outlined, 'Symptoms', AppColors.lavender,
value: _getSymptomsString(entry)),
// Contextual Recommendation
_buildRecommendation(entry),
@@ -507,40 +410,49 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Notes', style: GoogleFonts.outfit(fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.warmGray)),
Text('Notes',
style: GoogleFonts.outfit(
fontSize: 12,
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)),
],
),
),
],
const SizedBox(height: 24),
// Action Buttons
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: Text('Log for ${_getMonthName(date.month)} ${date.day}'),
if (!widget.readOnly)
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: Text(
'Log for ${_getMonthName(date.month)} ${date.day}'),
),
body: LogScreen(initialDate: date),
),
body: LogScreen(initialDate: date),
),
),
);
},
icon: Icon(entry != null ? Icons.edit_note : Icons.add_circle_outline),
label: Text(entry != null ? 'Edit Log' : 'Add Log'),
);
},
icon: Icon(entry != null
? 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)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
),
),
@@ -552,7 +464,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
}
Widget _buildRecommendation(CycleEntry entry) {
final scripture = ScriptureDatabase.getRecommendedScripture(entry);
final scripture = ScriptureDatabase().getRecommendedScripture(entry);
if (scripture == null) return const SizedBox.shrink();
final user = ref.read(userProfileProvider);
@@ -572,7 +484,8 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
children: [
Row(
children: [
const Icon(Icons.auto_awesome, color: AppColors.softGold, size: 18),
const Icon(Icons.auto_awesome,
color: AppColors.softGold, size: 18),
const SizedBox(width: 8),
Text(
'Daily Encouragement',
@@ -600,7 +513,6 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
>>>>>>> 6742220 (Your commit message here)
color: AppColors.warmGray,
),
),
@@ -609,9 +521,8 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
);
}
<<<<<<< HEAD
=======
Widget _buildDetailRow(IconData icon, String label, Color color, {String? value}) {
Widget _buildDetailRow(IconData icon, String label, Color color,
{String? value}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
@@ -652,7 +563,8 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
String _getSymptomsString(CycleEntry entry) {
List<String> s = [];
if (entry.crampIntensity != null && entry.crampIntensity! > 0) s.add('Cramps (${entry.crampIntensity}/5)');
if (entry.crampIntensity != null && entry.crampIntensity! > 0)
s.add('Cramps (${entry.crampIntensity}/5)');
if (entry.hasHeadache) s.add('Headache');
if (entry.hasBloating) s.add('Bloating');
if (entry.hasBreastTenderness) s.add('Breast Tenderness');
@@ -661,15 +573,24 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
return s.join(', ');
}
>>>>>>> 6742220 (Your commit message here)
CyclePhase? _getPhaseForDate(DateTime date, DateTime? lastPeriodStart, int cycleLength) {
String _getEnergyLabel(int? energyLevel) {
if (energyLevel == null) return 'Not logged';
if (energyLevel <= 1) return 'Very Low';
if (energyLevel == 2) return 'Low';
if (energyLevel == 3) return 'Neutral';
if (energyLevel == 4) return 'High';
return 'Very High';
}
CyclePhase? _getPhaseForDate(
DateTime date, DateTime? lastPeriodStart, int cycleLength) {
if (lastPeriodStart == null) return null;
final daysSinceLastPeriod = date.difference(lastPeriodStart).inDays;
if (daysSinceLastPeriod < 0) return null;
final dayOfCycle = (daysSinceLastPeriod % cycleLength) + 1;
if (dayOfCycle <= 5) return CyclePhase.menstrual;
if (dayOfCycle <= 13) return CyclePhase.follicular;
if (dayOfCycle <= 16) return CyclePhase.ovulation;
@@ -691,8 +612,18 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
String _getMonthName(int month) {
const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
return months[month - 1];
}