- Add 10-second periodic auto-sync to CycleEntriesNotifier - Fix husband_devotional_screen: use partnerId for isConnected check, navigate to SharingSettingsScreen instead of legacy mock dialog - Remove obsolete _showConnectDialog method and mock data import - Update husband_settings_screen: show 'Partner Settings' with linked partner name when connected - Add SharingSettingsScreen: Pad Supplies toggle (disabled when pad tracking off), Intimacy always enabled - Add CORS OPTIONS handler to backend server - Add _ensureServerRegistration for reliable partner linking - Add copy button to Invite Partner dialog - Dynamic base URL for web (uses window.location.hostname)
656 lines
23 KiB
Dart
656 lines
23 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.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 '../../models/teaching_plan.dart';
|
|
import '../../providers/scripture_provider.dart'; // Import the new provider
|
|
import '../prayer/prayer_request_screen.dart';
|
|
import '../settings/sharing_settings_screen.dart';
|
|
|
|
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
|
|
? const 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 isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
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: Theme.of(context).textTheme.titleLarge?.color,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: _getPhaseColor(phase).withValues(alpha: 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: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
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: const Icon(Icons.arrow_back_ios),
|
|
onPressed: () => ref
|
|
.read(scriptureProvider.notifier)
|
|
.getPreviousScripture(),
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
Positioned(
|
|
right: 0,
|
|
child: IconButton(
|
|
icon: const Icon(Icons.arrow_forward_ios),
|
|
onPressed: () => ref
|
|
.read(scriptureProvider.notifier)
|
|
.getNextScripture(),
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
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: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(
|
|
Icons.lightbulb_outline,
|
|
color: AppColors.softGold,
|
|
size: 20,
|
|
),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'Reflection',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.charcoal,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
scripture.reflection!,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 15,
|
|
color: Theme.of(context).textTheme.bodyLarge?.color,
|
|
height: 1.6,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
|
|
// Phase-specific encouragement
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const 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: Theme.of(context).textTheme.titleLarge?.color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_getPhaseEncouragement(phase, user?.isMarried ?? false),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 15,
|
|
color: Theme.of(context).textTheme.bodyLarge?.color,
|
|
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.withValues(alpha: isDark ? 0.35 : 0.2),
|
|
AppColors.blushPink.withValues(alpha: isDark ? 0.35 : 0.2),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Text('🙏', style: TextStyle(fontSize: 20)),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Prayer Prompt',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_getPrayerPrompt(phase),
|
|
style: GoogleFonts.lora(
|
|
fontSize: 14,
|
|
fontStyle: FontStyle.italic,
|
|
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
height: 1.6,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Husband's Teaching Plan
|
|
if (user != null)
|
|
if (user.teachingPlans?.isNotEmpty ?? false)
|
|
_buildTeachingPlanCard(context, user.teachingPlans!)
|
|
else
|
|
_buildSampleTeachingCard(context),
|
|
|
|
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: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const PrayerRequestScreen(),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.spa_outlined),
|
|
label: const Text('Prayer Requests'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
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."';
|
|
}
|
|
}
|
|
|
|
Widget _buildTeachingPlanCard(
|
|
BuildContext context, List<TeachingPlan> plans) {
|
|
// Get latest uncompleted plan or just latest
|
|
if (plans.isEmpty) return const SizedBox.shrink();
|
|
// Sort by date desc
|
|
final sorted = List<TeachingPlan>.from(plans)
|
|
..sort((a, b) => b.date.compareTo(a.date));
|
|
final latestPlan = sorted.first;
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: AppColors.gold.withValues(alpha: 0.5)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.gold.withValues(alpha: 0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.menu_book, color: AppColors.navyBlue),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'Leading in the Word',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.gold.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'Husband\'s Sharing',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 10,
|
|
color: AppColors.gold,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
latestPlan.topic,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
if (latestPlan.scriptureReference.isNotEmpty) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
latestPlan.scriptureReference,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: AppColors.gold,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
latestPlan.notes,
|
|
style: GoogleFonts.lora(
|
|
fontSize: 15,
|
|
height: 1.5,
|
|
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSampleTeachingCard(BuildContext context) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: AppColors.warmGray.withValues(alpha: 0.3),
|
|
style: BorderStyle.solid),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.menu_book, color: AppColors.warmGray),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
'Leading in the Word',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.warmGray,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.warmGray.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'Sample',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 10,
|
|
color: AppColors.warmGray,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Husband\'s Role in Leading',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSurface
|
|
.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Ephesians 5:25',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: AppColors.warmGray,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'This is a sample of where your husband\'s teaching plans will appear. Connect with his app to share spiritual growth together.',
|
|
style: GoogleFonts.lora(
|
|
fontSize: 15,
|
|
height: 1.5,
|
|
fontStyle: FontStyle.italic,
|
|
color: AppColors.charcoal.withValues(alpha: 0.6),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Center(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const SharingSettingsScreen(),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.link, size: 18),
|
|
label: const Text('Connect with Husband'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: AppColors.navyBlue,
|
|
side: const BorderSide(color: AppColors.navyBlue),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|