Files
Tracker/lib/screens/home/home_screen.dart

871 lines
31 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../theme/app_theme.dart';
import '../../models/user_profile.dart';
import '../../models/cycle_entry.dart';
import '../../models/scripture.dart';
import '../calendar/calendar_screen.dart';
import '../log/log_screen.dart';
import '../log/pad_tracker_screen.dart';
import '../devotional/devotional_screen.dart';
import '../settings/appearance_screen.dart';
import '../settings/cycle_settings_screen.dart';
import '../settings/relationship_settings_screen.dart';
import '../settings/goal_settings_screen.dart';
import '../settings/cycle_history_screen.dart';
import '../settings/sharing_settings_screen.dart';
import '../settings/notification_settings_screen.dart';
import '../settings/privacy_settings_screen.dart';
import '../settings/supplies_settings_screen.dart';
import '../settings/export_data_screen.dart';
import '../learn/wife_learn_screen.dart';
import '../../widgets/tip_card.dart';
import '../../widgets/cycle_ring.dart';
import '../../widgets/scripture_card.dart';
import '../../widgets/pad_tracker_card.dart';
import '../../widgets/quick_log_buttons.dart';
import '../../providers/user_provider.dart';
import '../../providers/navigation_provider.dart';
import '../../services/cycle_service.dart';
import '../../services/bible_utils.dart';
import '../../providers/scripture_provider.dart';
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedIndex = ref.watch(navigationProvider);
final isPadTrackingEnabled = ref.watch(userProfileProvider.select((u) => u?.isPadTrackingEnabled ?? false));
final List<Widget> tabs;
final List<BottomNavigationBarItem> navBarItems;
if (isPadTrackingEnabled) {
tabs = [
const _DashboardTab(),
const CalendarScreen(),
const PadTrackerScreen(),
const LogScreen(),
const DevotionalScreen(),
const WifeLearnScreen(),
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
];
navBarItems = [
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'),
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'),
const BottomNavigationBarItem(icon: Icon(Icons.inventory_2_outlined), activeIcon: Icon(Icons.inventory_2), label: 'Supplies'),
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'),
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'),
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'),
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'),
];
} else {
tabs = [
const _DashboardTab(),
const CalendarScreen(),
const DevotionalScreen(),
const LogScreen(),
const WifeLearnScreen(),
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
];
navBarItems = [
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'),
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'),
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'),
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'),
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'),
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'),
];
}
return Scaffold(
body: IndexedStack(
index: selectedIndex >= tabs.length ? 0 : selectedIndex,
children: tabs,
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
boxShadow: [
BoxShadow(
color: (Theme.of(context).brightness == Brightness.dark
? Colors.black
: AppColors.charcoal)
.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
currentIndex: selectedIndex >= tabs.length ? 0 : selectedIndex,
onTap: (index) =>
ref.read(navigationProvider.notifier).setIndex(index),
items: navBarItems,
),
),
);
}
}
class _DashboardTab extends ConsumerStatefulWidget {
const _DashboardTab({super.key});
@override
ConsumerState<_DashboardTab> createState() => _DashboardTabState();
}
class _DashboardTabState extends ConsumerState<_DashboardTab> {
@override
void initState() {
super.initState();
_initializeScripture();
}
// This method initializes the scripture and can react to phase changes.
// It's called from initState and also when currentCycleInfoProvider changes.
Future<void> _initializeScripture() async {
final phase = ref.read(currentCycleInfoProvider).phase;
await ref.read(scriptureProvider.notifier).initializeScripture(phase);
}
@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 name =
ref.watch(userProfileProvider.select((u) => u?.name)) ?? 'Friend';
final translation =
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation)) ??
BibleTranslation.esv;
final role = ref.watch(userProfileProvider.select((u) => u?.role)) ??
UserRole.wife;
final isMarried =
ref.watch(userProfileProvider.select((u) => u?.isMarried)) ?? false;
final averageCycleLength =
ref.watch(userProfileProvider.select((u) => u?.averageCycleLength)) ??
28;
final cycleInfo = ref.watch(currentCycleInfoProvider);
final phase = cycleInfo.phase;
final dayOfCycle = cycleInfo.dayOfCycle;
// 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: [
_buildGreeting(context, name),
const SizedBox(height: 24),
Center(
child: CycleRing(
dayOfCycle: dayOfCycle,
totalDays: averageCycleLength,
phase: phase,
),
),
if (phase == CyclePhase.menstrual) ...[
const SizedBox(height: 24),
const PadTrackerCard(),
],
const SizedBox(height: 32),
// Main Scripture Card with Navigation
Stack(
alignment: Alignment.center,
children: [
ScriptureCard(
verse: scripture.getVerse(translation),
reference: scripture.reference,
translation: translation.label,
phase: phase,
onTranslationTap: () =>
BibleUtils.showTranslationPicker(context, ref),
),
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: Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(height: 24),
Text(
'Quick Log',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
const QuickLogButtons(),
const SizedBox(height: 24),
if (role == UserRole.wife)
_buildWifeTipsSection(context),
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildGreeting(BuildContext context, String name) {
final theme = Theme.of(context);
final hour = DateTime.now().hour;
String greeting;
if (hour < 12) {
greeting = 'Good morning';
} else if (hour < 17) {
greeting = 'Good afternoon';
} else {
greeting = 'Good evening';
}
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$greeting,',
style: GoogleFonts.outfit(
fontSize: 16,
color: theme.colorScheme.onSurfaceVariant,
),
),
Text(
name,
style: theme.textTheme.displaySmall?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
],
),
),
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.notifications_outlined,
color: theme.colorScheme.primary,
),
),
],
);
}
}
class _SettingsTab extends ConsumerWidget {
final VoidCallback? onReset;
const _SettingsTab({this.onReset});
Widget _buildSettingsTile(BuildContext context, IconData icon, String title,
{VoidCallback? onTap}) {
return ListTile(
leading: Icon(icon,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8)),
title: Text(
title,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 16,
),
),
trailing: const Icon(Icons.chevron_right, color: AppColors.lightGray),
onTap: onTap ??
() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Settings coming soon!')),
);
},
);
}
Future<void> _resetApp(BuildContext context, WidgetRef ref) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Reset App?'),
content: const Text(
'This will clear all data and return you to onboarding. Are you sure?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Reset', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true) {
await ref.read(userProfileProvider.notifier).clearProfile();
await ref.read(cycleEntriesProvider.notifier).clearEntries();
if (context.mounted) {
onReset?.call();
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final name =
ref.watch(userProfileProvider.select((u) => u?.name)) ?? 'Guest';
final roleSymbol =
ref.watch(userProfileProvider.select((u) => u?.role)) ==
UserRole.husband
? 'HUSBAND'
: null;
final relationshipStatus = ref.watch(userProfileProvider
.select((u) => u?.relationshipStatus.name.toUpperCase())) ??
'SINGLE';
final translationLabel =
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ??
'ESV';
final isSingle = ref.watch(userProfileProvider.select((u) => u?.relationshipStatus == RelationshipStatus.single));
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Settings',
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontSize: 28,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.05)),
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.7),
Theme.of(context).colorScheme.secondary.withOpacity(0.7)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
Text(
roleSymbol ?? relationshipStatus,
style: GoogleFonts.outfit(
fontSize: 12,
letterSpacing: 1,
color: AppColors.warmGray,
),
),
],
),
),
const Icon(Icons.chevron_right, color: AppColors.warmGray),
],
),
),
const SizedBox(height: 24),
_buildSettingsGroup(context, 'Preferences', [
_buildSettingsTile(
context,
Icons.notifications_outlined,
'Notifications',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NotificationSettingsScreen()));
},
),
_buildSettingsTile(
context,
Icons.inventory_2_outlined,
'Period Supplies',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SuppliesSettingsScreen()));
},
),
_buildSettingsTile(
context,
Icons.favorite_outline, // Use a different icon for Relationship
'Relationship Status',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RelationshipSettingsScreen()));
},
),
_buildSettingsTile(
context,
Icons.flag_outlined,
'Cycle Goal',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const GoalSettingsScreen()));
},
),
_buildSettingsTile(
context,
Icons.book_outlined,
'Bible Version ($translationLabel)',
onTap: () => BibleUtils.showTranslationPicker(context, ref),
),
_buildSettingsTile(context, Icons.palette_outlined, 'Appearance',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AppearanceScreen()));
}),
_buildSettingsTile(
context,
Icons.favorite_border,
'My Favorites',
onTap: () => _showFavoritesDialog(context, ref),
),
_buildSettingsTile(
context, Icons.security, 'Privacy & Security',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PrivacySettingsScreen()));
}),
if (!isSingle)
_buildSettingsTile(
context,
Icons.share_outlined,
'Share with Husband',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const SharingSettingsScreen()));
},
),
]),
const SizedBox(height: 16),
_buildSettingsGroup(context, 'Cycle', [
_buildSettingsTile(
context, Icons.calendar_today_outlined, 'Cycle Settings',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CycleSettingsScreen()));
}),
_buildSettingsTile(
context, Icons.trending_up_outlined, 'Cycle History',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CycleHistoryScreen()));
}),
_buildSettingsTile(
context, Icons.download_outlined, 'Export Data',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ExportDataScreen()));
}),
]),
const SizedBox(height: 16),
_buildSettingsGroup(context, 'Account', [
_buildSettingsTile(context, Icons.logout, 'Reset App / Logout',
onTap: () => _resetApp(context, ref)),
]),
const SizedBox(height: 16),
],
),
),
);
}
Widget _buildSettingsGroup(
BuildContext context, String title, List<Widget> tiles) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.05)),
),
child: Column(
children: tiles,
),
),
],
);
}
Future<bool> _authenticate(BuildContext context, String correctPin) async {
final controller = TextEditingController();
final pin = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Enter PIN'),
content: TextField(
controller: controller,
keyboardType: TextInputType.number,
obscureText: true,
maxLength: 4,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 24, letterSpacing: 8),
decoration: const InputDecoration(hintText: '....'),
autofocus: true,
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
ElevatedButton(
onPressed: () => Navigator.pop(context, controller.text),
child: const Text('Unlock'),
),
],
),
);
return pin == correctPin;
}
void _showFavoritesDialog(BuildContext context, WidgetRef ref) async {
final userProfile = ref.read(userProfileProvider);
if (userProfile == null) return;
if (userProfile.isBioProtected && userProfile.privacyPin != null) {
final granted = await _authenticate(context, userProfile.privacyPin!);
if (!granted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Incorrect PIN')));
}
return;
}
}
final controller = TextEditingController(
text: userProfile.favoriteFoods?.join(', ') ?? '',
);
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('My Favorites', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'List your favorite comfort foods, snacks, or flowers so your husband knows what to get you!',
style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray),
),
const SizedBox(height: 16),
TextField(
controller: controller,
maxLines: 3,
decoration: const InputDecoration(
hintText: 'e.g., Dark Chocolate, Sushi, Sunflowers...',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
final favorites = controller.text
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
final updatedProfile = userProfile.copyWith(favoriteFoods: favorites);
ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
Navigator.pop(context);
},
child: const Text('Save'),
),
],
),
);
}
void _showShareDialog(BuildContext context, WidgetRef ref) {
// Generate a simple pairing code (in a real app, this would be stored/validated)
final userProfile = ref.read(userProfileProvider);
final pairingCode = userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123';
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.share_outlined, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 8),
const Text('Share with Husband'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Share this code with your husband so he can connect to your cycle data:',
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)),
),
child: SelectableText(
pairingCode,
style: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: 4,
color: Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(height: 16),
Text(
'He can enter this in his app under Settings > Connect with Wife.',
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
textAlign: TextAlign.center,
),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
),
child: const Text('Done'),
),
],
),
);
}
}
Widget _buildWifeTipsSection(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Health Tips',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTipCard(
context,
title: 'Regular Check-ups',
content:
'Schedule regular gynecological check-ups to monitor your reproductive health.',
icon: Icons.medical_services,
),
const SizedBox(height: 16),
_buildTipCard(
context,
title: 'Healthy Lifestyle',
content:
'Maintain a balanced diet, exercise regularly, and get adequate sleep.',
icon: Icons.healing,
),
const SizedBox(height: 16),
_buildTipCard(
context,
title: 'Partner Communication',
content:
'Discuss health concerns openly with your partner to ensure mutual understanding.',
icon: Icons.chat,
),
],
),
),
),
],
);
}
Widget _buildTipCard(
BuildContext context, {
required String title,
required String content,
required IconData icon,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: Theme.of(context).colorScheme.primary,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontWeight: FontWeight.w600,
fontSize: 14,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
content,
style: GoogleFonts.outfit(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
);
}