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:
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user