Files
Tracker/lib/screens/devotional/devotional_screen.dart
Sterlen b4b2bfe749 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.
2025-12-26 22:40:52 -06:00

435 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../models/scripture.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/user_provider.dart';
import '../../services/cycle_service.dart';
import '../../models/cycle_entry.dart';
import '../../theme/app_theme.dart';
import '../../widgets/scripture_card.dart';
import '../../models/user_profile.dart';
import '../../providers/scripture_provider.dart'; // Import the new provider
class DevotionalScreen extends ConsumerStatefulWidget {
const DevotionalScreen({super.key});
@override
ConsumerState<DevotionalScreen> createState() => _DevotionalScreenState();
}
class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
@override
void initState() {
super.initState();
_initializeScripture();
}
Future<void> _initializeScripture() async {
final phase = ref.read(currentCycleInfoProvider).phase;
await ref.read(scriptureProvider.notifier).initializeScripture(phase);
}
Future<void> _showTranslationPicker(
BuildContext context, WidgetRef ref, UserProfile? user) async {
if (user == null) return;
final selected = await showModalBottomSheet<BibleTranslation>(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Text(
'Select Bible Translation',
style: Theme.of(context).textTheme.titleLarge,
),
),
...BibleTranslation.values.map((t) => ListTile(
title: Text(t.label),
trailing: user.bibleTranslation == t
? Icon(Icons.check, color: AppColors.sageGreen)
: null,
onTap: () => Navigator.pop(context, t),
)),
],
),
),
);
if (selected != null) {
await ref
.read(userProfileProvider.notifier)
.updateProfile(user.copyWith(bibleTranslation: selected));
}
}
@override
Widget build(BuildContext context) {
// Listen for changes in the cycle info to re-initialize scripture if needed
ref.listen<CycleInfo>(currentCycleInfoProvider, (previousCycleInfo, newCycleInfo) {
if (previousCycleInfo?.phase != newCycleInfo.phase) {
_initializeScripture();
}
});
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final phase = cycleInfo.phase;
// Watch the scripture provider for the current scripture
final scriptureState = ref.watch(scriptureProvider);
final scripture = scriptureState.currentScripture;
final maxIndex = scriptureState.maxIndex;
if (scripture == null) {
return const Center(child: CircularProgressIndicator()); // Or some error message
}
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Expanded(
child: Text(
'Today\'s Devotional',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Text(phase.emoji),
const SizedBox(width: 4),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
],
),
const SizedBox(height: 8),
Text(
phase.description,
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 32),
// Main Scripture Card with Navigation
Stack(
alignment: Alignment.center,
children: [
ScriptureCard(
verse: scripture
.getVerse(user?.bibleTranslation ?? BibleTranslation.esv),
reference: scripture.reference,
translation:
(user?.bibleTranslation ?? BibleTranslation.esv).label,
phase: phase,
onTranslationTap: () =>
_showTranslationPicker(context, ref, user),
),
if (maxIndex != null && maxIndex > 1) ...[
Positioned(
left: 0,
child: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () =>
ref.read(scriptureProvider.notifier).getPreviousScripture(),
color: AppColors.charcoal,
),
),
Positioned(
right: 0,
child: IconButton(
icon: Icon(Icons.arrow_forward_ios),
onPressed: () =>
ref.read(scriptureProvider.notifier).getNextScripture(),
color: AppColors.charcoal,
),
),
],
],
),
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: AppColors.sageGreen,
),
),
),
const SizedBox(height: 24),
// Reflection
if (scripture.reflection != null) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.lightbulb_outline,
color: AppColors.softGold,
size: 20,
),
const SizedBox(width: 8),
Text(
'Reflection',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
const SizedBox(height: 12),
Text(
scripture.reflection!,
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
height: 1.6,
),
),
],
),
),
const SizedBox(height: 16),
],
// Phase-specific encouragement
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.favorite_outline,
color: AppColors.rose,
size: 20,
),
const SizedBox(width: 8),
Text(
'For Your ${phase.label} Phase',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
const SizedBox(height: 12),
Text(
_getPhaseEncouragement(phase, user?.isMarried ?? false),
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
height: 1.6,
),
),
],
),
),
const SizedBox(height: 16),
// Prayer Prompt
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.lavender.withOpacity(0.2),
AppColors.blushPink.withOpacity(0.2),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('🙏', style: TextStyle(fontSize: 20)),
const SizedBox(width: 8),
Text(
'Prayer Prompt',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
const SizedBox(height: 12),
Text(
_getPrayerPrompt(phase),
style: GoogleFonts.lora(
fontSize: 14,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
],
),
),
const SizedBox(height: 24),
// Action buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.share_outlined),
label: const Text('Share'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.edit_note),
label: const Text('Journal'),
),
),
],
),
const SizedBox(height: 40),
],
),
),
);
}
// Placeholder _getCurrentPhase removed as it's now in CycleService
Color _getPhaseColor(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return AppColors.menstrualPhase;
case CyclePhase.follicular:
return AppColors.follicularPhase;
case CyclePhase.ovulation:
return AppColors.ovulationPhase;
case CyclePhase.luteal:
return AppColors.lutealPhase;
}
}
String _getPhaseEncouragement(CyclePhase phase, bool isMarried) {
switch (phase) {
case CyclePhase.menstrual:
return 'Your body is renewing itself. This is a sacred time for rest and reflection. '
'Don\'t push yourself too hard—God designed this phase for slowing down. '
'Use this time to draw near to Him in quietness.';
case CyclePhase.follicular:
return 'Energy is returning! Your body is preparing for the days ahead. '
'This is a wonderful time to tackle projects, connect with friends, and serve others. '
'Let your renewed strength be used for His purposes.';
case CyclePhase.ovulation:
if (isMarried) {
return 'You are in your most fertile window. Whether you\'re hoping to conceive or practicing NFP, '
'remember that God is sovereign over the womb. Trust His timing and purposes for your family.';
}
return 'You may feel more confident and social during this phase. '
'It\'s a great time for important conversations, presentations, or stepping out in faith. '
'Let your light shine before others.';
case CyclePhase.luteal:
return 'The luteal phase can bring challenging emotions and PMS symptoms. '
'Be patient with yourself. This is not weakness—it\'s your body doing what God designed. '
'Lean into His peace that surpasses understanding.';
}
}
String _getPrayerPrompt(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return '"Lord, thank You for designing my body with such wisdom. '
'Help me to rest in You during this time and to trust that You are renewing me. '
'May I find my strength in Your presence. Amen."';
case CyclePhase.follicular:
return '"Father, thank You for this season of renewed energy. '
'Guide me to use this strength for Your glory and the good of others. '
'Help me to serve with joy and purpose. Amen."';
case CyclePhase.ovulation:
return '"Creator God, I am fearfully and wonderfully made. '
'Thank You for the gift of womanhood. '
'Help me to honor You in all I do today. Amen."';
case CyclePhase.luteal:
return '"Lord, I bring my anxious thoughts to You. '
'When my emotions feel overwhelming, remind me of Your peace. '
'Help me to be gentle with myself as You are gentle with me. Amen."';
}
}
}