Implement husband-wife connection dialogue and theme support for learn articles

This commit is contained in:
2026-01-05 17:09:15 -06:00
parent 02d25d0cc7
commit 96655f9a74
36 changed files with 3849 additions and 819 deletions

View File

@@ -0,0 +1,369 @@
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/teaching_plan.dart';
import '../../providers/user_provider.dart';
import '../../theme/app_theme.dart';
class HusbandDevotionalScreen extends ConsumerStatefulWidget {
const HusbandDevotionalScreen({super.key});
@override
ConsumerState<HusbandDevotionalScreen> createState() => _HusbandDevotionalScreenState();
}
class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScreen> {
void _showAddTeachingDialog([TeachingPlan? existingPlan]) {
final titleController = TextEditingController(text: existingPlan?.topic);
final scriptureController = TextEditingController(text: existingPlan?.scriptureReference);
final notesController = TextEditingController(text: existingPlan?.notes);
DateTime selectedDate = existingPlan?.date ?? DateTime.now();
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: Text(existingPlan == null ? 'Plan Teaching' : 'Edit Plan'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: 'Topic / Theme',
hintText: 'e.g., Patience, Prayer, Grace',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: scriptureController,
decoration: const InputDecoration(
labelText: 'Scripture Reference',
hintText: 'e.g., Eph 5:25',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: notesController,
maxLines: 3,
decoration: const InputDecoration(
labelText: 'Notes / Key Points',
hintText: 'What do you want to share?',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Row(
children: [
Text('Date: ${DateFormat.yMMMd().format(selectedDate)}'),
const Spacer(),
TextButton(
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
setState(() => selectedDate = picked);
}
},
child: const Text('Change'),
),
],
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
if (titleController.text.isEmpty) return;
final user = ref.read(userProfileProvider);
if (user == null) return;
TeachingPlan newPlan;
if (existingPlan != null) {
newPlan = existingPlan.copyWith(
topic: titleController.text,
scriptureReference: scriptureController.text,
notes: notesController.text,
date: selectedDate,
);
} else {
newPlan = TeachingPlan.create(
topic: titleController.text,
scriptureReference: scriptureController.text,
notes: notesController.text,
date: selectedDate,
);
}
List<TeachingPlan> 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),
);
if (mounted) Navigator.pop(context);
},
child: const Text('Save'),
),
],
),
),
);
}
void _deletePlan(TeachingPlan plan) async {
final user = ref.read(userProfileProvider);
if (user == null || user.teachingPlans == null) return;
final updatedList = user.teachingPlans!.where((p) => p.id != plan.id).toList();
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList),
);
}
void _toggleComplete(TeachingPlan plan) async {
final user = ref.read(userProfileProvider);
if (user == null || user.teachingPlans == null) return;
final updatedList = user.teachingPlans!.map((p) {
if (p.id == plan.id) return p.copyWith(isCompleted: !p.isCompleted);
return p;
}).toList();
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList),
);
}
@override
Widget build(BuildContext context) {
final user = ref.watch(userProfileProvider);
final upcomingPlans = user?.teachingPlans ?? [];
upcomingPlans.sort((a,b) => a.date.compareTo(b.date));
return Scaffold(
appBar: AppBar(
title: const Text('Spiritual Leadership'),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informational Card (Headship)
_buildHeadshipCard(),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Teaching Plans',
style: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.navyBlue,
),
),
IconButton(
onPressed: () => _showAddTeachingDialog(),
icon: const Icon(Icons.add_circle, color: AppColors.navyBlue, size: 28),
),
],
),
const SizedBox(height: 12),
if (upcomingPlans.isEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.withOpacity(0.2)),
),
child: Column(
children: [
const Icon(Icons.edit_note, size: 48, color: Colors.grey),
const SizedBox(height: 12),
Text(
'No teachings planned yet.',
style: GoogleFonts.outfit(color: AppColors.warmGray),
),
TextButton(
onPressed: () => _showAddTeachingDialog(),
child: const Text('Plan one now'),
),
],
),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: upcomingPlans.length,
separatorBuilder: (ctx, i) => const SizedBox(height: 12),
itemBuilder: (ctx, index) {
final plan = upcomingPlans[index];
return Dismissible(
key: Key(plan.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
color: Colors.red.withOpacity(0.8),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) => _deletePlan(plan),
child: Card(
elevation: 2,
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),
),
title: Text(
plan.topic,
style: GoogleFonts.outfit(
fontWeight: FontWeight.w600,
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)),
if (plan.notes.isNotEmpty)
Text(
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]),
),
],
),
isThreeLine: true,
),
),
);
},
),
const SizedBox(height: 40),
],
),
),
);
}
Widget _buildHeadshipCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFFDF8F0), // Warm tone
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE0C097)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.menu_book, color: Color(0xFF8B5E3C)),
const SizedBox(width: 12),
Text(
'Biblical Principles',
style: GoogleFonts.lora(
fontSize: 18,
fontWeight: FontWeight.bold,
color: const Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 16),
_buildVerseText(
'1 Corinthians 11:3',
'“The head of every man is Christ, the head of a wife is her husband, and the head of Christ is God.”',
'Supports family structure under Christs authority.',
),
const SizedBox(height: 16),
const Divider(height: 1, color: Color(0xFFE0C097)),
const SizedBox(height: 16),
_buildVerseText(
'1 Tim 3:45, 12 & Titus 1:6',
'Qualifications for church elders include managing their own households well.',
'Husbands who lead faithfully at home are seen as candidates for formal spiritual leadership.',
),
],
),
);
}
Widget _buildVerseText(String ref, String text, String context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ref,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.bold,
color: const Color(0xFF8B5E3C),
),
),
const SizedBox(height: 4),
Text(
text,
style: GoogleFonts.lora(
fontSize: 15,
fontStyle: FontStyle.italic,
height: 1.4,
color: const Color(0xFF3E2723),
),
),
const SizedBox(height: 4),
Text(
context,
style: GoogleFonts.outfit(
fontSize: 12,
color: const Color(0xFF6D4C41),
),
),
],
);
}
}

View File

@@ -10,6 +10,7 @@ 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 'husband_devotional_screen.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Husband's companion app main screen
@@ -34,7 +35,7 @@ class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
children: [
const _HusbandDashboard(),
const CalendarScreen(readOnly: true), // Reused Calendar
const HusbandNotesScreen(), // Notes Screen
const HusbandDevotionalScreen(), // Devotional & Planning
const _HusbandTipsScreen(),
const _HusbandLearnScreen(),
const _HusbandSettingsScreen(),
@@ -70,9 +71,9 @@ class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
label: 'Calendar',
),
BottomNavigationBarItem(
icon: Icon(Icons.note_alt_outlined),
activeIcon: Icon(Icons.note_alt),
label: 'Notes',
icon: Icon(Icons.menu_book_outlined),
activeIcon: Icon(Icons.menu_book),
label: 'Devotion',
),
BottomNavigationBarItem(
icon: Icon(Icons.lightbulb_outline),
@@ -1253,10 +1254,12 @@ class _HusbandSettingsScreen extends ConsumerWidget {
void _showConnectDialog(BuildContext context, WidgetRef ref) {
final codeController = TextEditingController();
bool shareDevotional = true;
showDialog(
context: context,
builder: (context) => AlertDialog(
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: Row(
children: [
const Icon(Icons.link, color: AppColors.navyBlue),
@@ -1286,6 +1289,37 @@ class _HusbandSettingsScreen extends ConsumerWidget {
'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,
),
),
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: [
@@ -1296,36 +1330,44 @@ class _HusbandSettingsScreen extends ConsumerWidget {
ElevatedButton(
onPressed: () async {
final code = codeController.text.trim();
if (code.isEmpty) return;
// In a real app, this would validate the code against a backend
// For now, we'll just show a success message and simulate pairing
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(
SnackBar(
content: Text('Connected! Loading wife\'s data...'),
content: Text('Settings updated & Connected!'),
backgroundColor: AppColors.sageGreen,
),
);
// Load demo data as simulation of pairing
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,
favoriteFoods: mockWife.favoriteFoods,
);
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
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);
}
final mockWife = mockService.generateMockWifeProfile();
final currentProfile = ref.read(userProfileProvider);
if (currentProfile != null) {
final updatedProfile = currentProfile.copyWith(
isDataShared: shareDevotional,
partnerName: mockWife.name,
averageCycleLength: mockWife.averageCycleLength,
averagePeriodLength: mockWife.averagePeriodLength,
lastPeriodStartDate: mockWife.lastPeriodStartDate,
favoriteFoods: mockWife.favoriteFoods,
);
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
}
}
},
style: ElevatedButton.styleFrom(
@@ -1336,6 +1378,7 @@ class _HusbandSettingsScreen extends ConsumerWidget {
),
],
),
),
);
}

View File

@@ -21,12 +21,12 @@ class LearnArticleScreen extends StatelessWidget {
}
return Scaffold(
backgroundColor: AppColors.warmCream,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar(
backgroundColor: AppColors.warmCream,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: AppColors.navyBlue),
icon: Icon(Icons.arrow_back, color: Theme.of(context).iconTheme.color),
onPressed: () => Navigator.pop(context),
),
title: Text(
@@ -34,7 +34,7 @@ class LearnArticleScreen extends StatelessWidget {
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
centerTitle: true,
@@ -50,7 +50,7 @@ class LearnArticleScreen extends StatelessWidget {
style: GoogleFonts.outfit(
fontSize: 26,
fontWeight: FontWeight.w700,
color: AppColors.navyBlue,
color: Theme.of(context).textTheme.headlineMedium?.color,
height: 1.2,
),
),
@@ -59,7 +59,7 @@ class LearnArticleScreen extends StatelessWidget {
article.subtitle,
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.warmGray,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const SizedBox(height: 24),
@@ -69,21 +69,21 @@ class LearnArticleScreen extends StatelessWidget {
height: 3,
width: 40,
decoration: BoxDecoration(
color: AppColors.gold,
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 24),
// Sections
...article.sections.map((section) => _buildSection(section)),
...article.sections.map((section) => _buildSection(context, section)),
],
),
),
);
}
Widget _buildSection(LearnSection section) {
Widget _buildSection(BuildContext context, LearnSection section) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
@@ -95,18 +95,18 @@ class LearnArticleScreen extends StatelessWidget {
style: GoogleFonts.outfit(
fontSize: 17,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
color: Theme.of(context).textTheme.titleLarge?.color,
),
),
const SizedBox(height: 10),
],
_buildRichText(section.content),
_buildRichText(context, section.content),
],
),
);
}
Widget _buildRichText(String content) {
Widget _buildRichText(BuildContext context, String content) {
// Handle basic markdown-like formatting
final List<InlineSpan> spans = [];
final RegExp boldPattern = RegExp(r'\*\*(.*?)\*\*');
@@ -119,7 +119,7 @@ class LearnArticleScreen extends StatelessWidget {
text: content.substring(currentIndex, match.start),
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
color: Theme.of(context).textTheme.bodyLarge?.color,
height: 1.7,
),
));
@@ -130,7 +130,7 @@ class LearnArticleScreen extends StatelessWidget {
style: GoogleFonts.outfit(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
color: Theme.of(context).textTheme.titleMedium?.color,
height: 1.7,
),
));
@@ -143,7 +143,7 @@ class LearnArticleScreen extends StatelessWidget {
text: content.substring(currentIndex),
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
color: Theme.of(context).textTheme.bodyLarge?.color,
height: 1.7,
),
));